Laravel: Elegantna validacija e-pošte za registraciju tijekom prekida rada API-ja
Obrazac za registraciju nalazi se na osjetljivoj granici: trebao bi zaustaviti očito neupotrebljive adrese, ali ne bi smio isključiti stvarnog korisnika zato što je ovisna usluga imala loš dan. Ispravan produkcijski dizajn stoga nije ni „uvijek vjeruj API-ju” ni „dopusti sve bez vidljivosti”. To je mali, izričit sustav odlučivanja.
Ovaj vodič gradi taj sustav u Laravelu. Aplikacija lokalno provjerava osnovni unos, od Email Validatora traži dokaze o riziku isporučivosti, odbija samo jasnu i strukturno pouzdanu negativnu preporuku te dopušta registraciju sa stanjem rizika na čekanju kada usluga nije dostupna ili je njezin odgovor nesiguran.
Preduvjeti
Potrebni su vam PHP 8.3 ili noviji, Composer, Laravel aplikacija s konfiguriranom bazom podataka i standardna tablica users. Primjeri koriste Laravelov ugrađeni HTTP klijent, fasadu za autentifikaciju, validaciju, migracije, zapisivanje u logove, događaje i HTTP lažne odgovore za testiranje. Nije potreban integracijski paket treće strane.
Ako počinjete od nule, izradite aplikaciju i pripremite njezinu bazu podataka:
composer create-project laravel/laravel graceful-registration
cd graceful-registration
cp .env.example .env
php artisan key:generate
php artisan migrate
Dobijte pristup prije pisanja integracijskog koda
- Izradite račun na https://ai.mihajlo.mk/register, ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
- Otvorite stranicu usluge Email Validator. Odaberite dostupni Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju za Email Validator.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu. Ova usluga zahtijeva taj token; nije riječ o API-ju bez autentifikacije.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Rotaciju tretirajte kao operaciju uvođenja: odmah ažurirajte tajnu aplikacije, ponovno izgradite predmemoriranu konfiguraciju, provjerite zahtjev i tek tada smatrajte rotaciju dovršenom.
Potvrdite krajnju točku minimalnim zahtjevom
Točan poziv je GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Autentifikacija koristi parametar upita token={serviceToken}, dok se adresa dostavlja putem parametra upita email.
Za jednokratni test u zaštićenom terminalu postavite token u privremenu varijablu ljuske umjesto da ga upisujete izravno u URL:
export EMAIL_VALIDATOR_TEST_TOKEN="YOUR_SERVICE_TOKEN"
curl --get \
"https://ai.mihajlo.mk/api/email-validator/v1/check-email" \
--data-urlencode "token=${EMAIL_VALIDATOR_TEST_TOKEN}" \
--data-urlencode "[email protected]"
Ugovor odgovora pruža status, score, recommendation, checks i quota. Usluga uzima u obzir sintaksu, podatke o domeni i MX-u, signale pružatelja usluge i praktični rizik isporučivosti. Naša granica zahtijevat će svih pet polja prije nego što preporuci vjeruje.
Pohranite vjerodajnicu u Laravelovu konfiguraciju
Dodajte tajnu u .env. Nikada ne predajte stvarnu vrijednost u repozitorij:
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_URL=https://ai.mihajlo.mk
EMAIL_VALIDATOR_CONNECT_TIMEOUT=2
EMAIL_VALIDATOR_TIMEOUT=4
EMAIL_VALIDATOR_RETRY_DELAY_MS=200
Dodajte ovaj unos u niz koji vraća config/services.php:
'email_validator' => [
'url' => env('EMAIL_VALIDATOR_URL', 'https://ai.mihajlo.mk'),
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'connect_timeout' => (int) env('EMAIL_VALIDATOR_CONNECT_TIMEOUT', 2),
'timeout' => (int) env('EMAIL_VALIDATOR_TIMEOUT', 4),
'retry_delay_ms' => (int) env('EMAIL_VALIDATOR_RETRY_DELAY_MS', 200),
],
Kôd aplikacije mora čitati config(), a ne izravno pozivati env(). To održava integraciju kompatibilnom s Laravelovom predmemorijom konfiguracije.
Odaberite namjerno konzervativnu arhitekturu
Provjera e-pošte ovdje je sinkrona jer odluka može odmah poboljšati odgovor na registraciju. Zadatak u redu čekanja završio bi prekasno da zaustavi jasno odbijenu adresu te bi dodao operativnu složenost bez poboljšanja korisničkog iskustva.
Putanja zahtjeva ima tri ishoda:
- Prihvati: odgovor je potpun i njegova preporuka odgovara rječniku koji aplikacija prihvaća.
- Odbij: odgovor je potpun i sadrži prepoznatu negativnu preporuku.
- Odgodi: API prekorači vrijeme čekanja, dosegne ograničenje kvote ili učestalosti, odbije autentifikaciju, vrati neispravne podatke, prijavi neprepoznat status ili navede nepoznatu preporuku.
Samo drugi ishod blokira registraciju. Odgođeni korisnici stvaraju se sa stanjem rizika na čekanju i mogu nastaviti kroz uobičajeni tijek potvrde e-pošte aplikacije.
Primjer prepoznaje accept/valid i reject/invalid kao lokalne oznake pravila. Nepoznate oznake sigurno odgađaju odluku. Provjerite službene primjere odgovora za aktiviranu uslugu i prilagodite ovaj mali popis dopuštenih vrijednosti ako se dokumentirani rječnik razlikuje. Namjerno ne izmišljamo prag rezultata niti tumačimo nedokumentirane ključeve unutar checks ili quota.
Preslikajte API odgovor u domensku odluku
Izradite app/Domain/EmailAssessment.php. Preslikavač na granici odbija djelomična ili neobično tipizirana opterećenja. Metapodaci o rezultatu, provjerama i kvoti moraju biti prisutni prije nego što se preporuka može primijeniti; u suprotnom rezultat postaje odgođen.
<?php
declare(strict_types=1);
namespace App\Domain;
use UnexpectedValueException;
enum EmailVerdict: string
{
case Accept = 'accept';
case Reject = 'reject';
case Defer = 'defer';
}
final readonly class EmailAssessment
{
public function __construct(
public EmailVerdict $verdict,
public string $status,
public ?float $score,
public string $recommendation,
public array $checks,
public array $quota,
public string $reason,
) {}
public static function fromPayload(array $payload): self
{
foreach (['status', 'score', 'recommendation', 'checks', 'quota'] as $field) {
if (!array_key_exists($field, $payload)) {
throw new UnexpectedValueException("Missing field: {$field}");
}
}
if (!is_string($payload['status'])
|| !is_string($payload['recommendation'])
|| !(is_int($payload['score']) || is_float($payload['score']))
|| !is_array($payload['checks'])
|| !is_array($payload['quota'])) {
throw new UnexpectedValueException('Unexpected response types');
}
$status = strtolower(trim($payload['status']));
$recommendation = strtolower(trim($payload['recommendation']));
$score = (float) $payload['score'];
if (!is_finite($score)
|| $payload['checks'] === []
|| $payload['quota'] === []
|| $status !== 'success') {
return new self(
EmailVerdict::Defer,
$status,
$score,
$recommendation,
$payload['checks'],
$payload['quota'],
'untrusted_response'
);
}
$verdict = match ($recommendation) {
'accept', 'valid' => EmailVerdict::Accept,
'reject', 'invalid' => EmailVerdict::Reject,
default => EmailVerdict::Defer,
};
return new self(
$verdict,
$status,
$score,
$recommendation,
$payload['checks'],
$payload['quota'],
$verdict === EmailVerdict::Defer
? 'unknown_recommendation'
: 'recommendation'
);
}
public static function deferred(string $reason): self
{
return new self(
EmailVerdict::Defer,
'unavailable',
null,
'',
[],
[],
$reason
);
}
}
To je sloj protiv korupcije: kontroleri nikada ne zaključuju na temelju proizvoljnog JSON-a. Primijetite da rezultat sudjeluje u procjeni pouzdanosti odgovora, ali se ne uspoređuje s nedokumentiranim pragom. Isto ograničenje vrijedi za ugniježđena polja provjera i kvote.
Izgradite ograničen, selektivan HTTP klijent
Izradite app/Services/EmailValidatorClient.php:
<?php
declare(strict_types=1);
namespace App\Services;
use App\Domain\EmailAssessment;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use UnexpectedValueException;
final class EmailValidatorClient
{
public function check(string $email): EmailAssessment
{
$token = config('services.email_validator.token');
if (!is_string($token) || $token === '') {
Log::error('email_validator.configuration_missing');
return EmailAssessment::deferred('configuration_missing');
}
$response = null;
$delay = max(0, (int) config(
'services.email_validator.retry_delay_ms',
200
));
for ($attempt = 1; $attempt <= 2; $attempt++) {
try {
$response = Http::baseUrl(
(string) config('services.email_validator.url')
)
->acceptJson()
->connectTimeout((int) config(
'services.email_validator.connect_timeout',
2
))
->timeout((int) config(
'services.email_validator.timeout',
4
))
->get('/api/email-validator/v1/check-email', [
'token' => $token,
'email' => $email,
]);
} catch (ConnectionException) {
if ($attempt === 1) {
if ($delay > 0) {
usleep($delay * 1000);
}
continue;
}
Log::warning('email_validator.connection_failed');
return EmailAssessment::deferred('connection_failed');
}
if ($attempt === 1
&& in_array($response->status(), [502, 503, 504], true)) {
if ($delay > 0) {
usleep($delay * 1000);
}
continue;
}
break;
}
if ($response->status() === 429) {
Log::warning('email_validator.quota_or_rate_limited');
return EmailAssessment::deferred('quota_or_rate_limited');
}
if (in_array($response->status(), [401, 403], true)) {
Log::error('email_validator.authentication_failed');
return EmailAssessment::deferred('authentication_failed');
}
if (!$response->successful()) {
Log::warning('email_validator.http_failure', [
'http_status' => $response->status(),
]);
return EmailAssessment::deferred('http_failure');
}
$payload = $response->json();
if (!is_array($payload)) {
return EmailAssessment::deferred('malformed_response');
}
try {
return EmailAssessment::fromPayload($payload);
} catch (UnexpectedValueException) {
Log::warning('email_validator.contract_mismatch');
return EmailAssessment::deferred('contract_mismatch');
}
}
}
Klijent izvodi najviše dva pokušaja. Jednom ponavlja neuspjele veze i odgovore nalik onima pristupnika 502, 503 i 504, nakon kratkog čekanja. Ne ponavlja neuspjele autentifikacije, neispravne zahtjeve ni odgovore 429. Trenutačno ponavljanje tih zahtjeva uzalud troši kapacitet i rijetko mijenja rezultat.
Trajno pohranite graciozno stanje
Dodajte stupac koji razlikuje pouzdano prihvaćanje od registracije dopuštene tijekom neizvjesnosti:
php artisan make:migration add_email_risk_state_to_users_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->string('email_risk_state', 20)
->default('pending')
->index();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn('email_risk_state');
});
}
};
Kasniji zakazani pregled može ponovno provjeriti račune na čekanju, ali sama registracija ne ovisi o tom budućem poboljšanju.
Povežite kontroler registracije
Izradite ili prilagodite app/Http/Controllers/RegisteredUserController.php:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Domain\EmailVerdict;
use App\Models\User;
use App\Services\EmailValidatorClient;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
final class RegisteredUserController extends Controller
{
public function store(
Request $request,
EmailValidatorClient $validator
): RedirectResponse {
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email:rfc', 'max:255',
'unique:users,email'],
'password' => ['required', 'string', 'min:12', 'confirmed'],
]);
$assessment = $validator->check($data['email']);
if ($assessment->verdict === EmailVerdict::Reject) {
throw ValidationException::withMessages([
'email' => 'Please use another email address.',
]);
}
if ($assessment->verdict === EmailVerdict::Defer) {
Log::notice('registration.email_validation_deferred', [
'reason' => $assessment->reason,
]);
}
$user = new User();
$user->name = $data['name'];
$user->email = $data['email'];
$user->password = Hash::make($data['password']);
$user->email_risk_state =
$assessment->verdict === EmailVerdict::Accept
? 'accepted'
: 'pending';
$user->save();
event(new Registered($user));
Auth::login($user);
return redirect('/dashboard');
}
}
Zadržite vanjski zahtjev izvan transakcije baze podataka; nijedna blokada baze podataka ne bi smjela ostati otvorena dok se čeka mreža. Događaj Registered također zadržava Laravelov uobičajeni tijek potvrđene e-pošte kada korisnički model implementira provjeru e-pošte.
Registrirajte krajnju točku u routes/web.php, prilagođavajući je ako komplet za pokretanje autentifikacije već upravlja ovim rutama:
use App\Http\Controllers\RegisteredUserController;
use Illuminate\Support\Facades\Route;
Route::view('/register', 'auth.register')
->middleware('guest')
->name('register');
Route::post('/register', [RegisteredUserController::class, 'store'])
->middleware(['guest', 'throttle:10,1'])
->name('register.store');
Dokažite pravilo neuspjeha automatiziranim testovima
Laravelov Http::fake() održava testove determinističkima i sprječava stvarnu potrošnju kvote. Dodajte slučajeve poput ovih u tests/Feature/RegistrationTest.php:
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class RegistrationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
config([
'services.email_validator.token' => 'test-token',
'services.email_validator.retry_delay_ms' => 0,
]);
}
public function test_clear_rejection_blocks_registration(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*'
=> Http::response([
'status' => 'success',
'score' => 10,
'recommendation' => 'reject',
'checks' => ['completed' => true],
'quota' => ['present' => true],
], 200),
]);
$response = $this->post('/register', $this->registrationData());
$response->assertSessionHasErrors('email');
$this->assertDatabaseCount('users', 0);
}
public function test_service_downtime_does_not_reject_user(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*'
=> Http::sequence()
->push([], 503)
->push([], 503),
]);
$response = $this->post('/register', $this->registrationData());
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
'email_risk_state' => 'pending',
]);
}
public function test_complete_acceptance_is_recorded(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*'
=> Http::response([
'status' => 'success',
'score' => 92,
'recommendation' => 'accept',
'checks' => ['completed' => true],
'quota' => ['present' => true],
], 200),
]);
$this->post('/register', $this->registrationData())
->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
'email_risk_state' => 'accepted',
]);
}
private function registrationData(): array
{
return [
'name' => 'Example Reader',
'email' => '[email protected]',
'password' => 'a-long-test-password',
'password_confirmation' => 'a-long-test-password',
];
}
}
Ova opterećenja su pomoćni podaci pravila aplikacije, a ne tvrdnje o nedokumentiranim ugniježđenim ključevima provjere ili kvote. Dodajte dodatne testove za 429, neispravan JSON, polja koja nedostaju, iznimke veze i nepoznate preporuke.
Sigurnost, vidljivost i uvođenje
Token putuje u parametru upita jer je to autentifikacijski ugovor usluge. HTTPS ga štiti tijekom prijenosa, ali URL-ovi se i dalje mogu pojaviti u zapisima proxyja ili praćenja. Konfigurirajte infrastrukturu da prikriva nizove upita i nikada ne zapisujte URL zahtjeva, token, adresu e-pošte ni tijelo odgovora. Gore navedeni strukturirani događaji otkrivaju operativne uzroke bez izlaganja osobnih podataka.
Ograničite učestalost registracije, zadržite Laravelovu CSRF zaštitu, host krajnje točke držite fiksnim u konfiguraciji i nastavite zahtijevati uobičajenu provjeru vlasništva nad e-poštom. Provjera rizika i provjera vlasništva rješavaju različite probleme.
Pratite broj odgođenih rezultata prema razlogu, neuspjele autentifikacije, raspodjelu HTTP statusa i udio računa koji ostaju na čekanju. Nagli porast contract_mismatch zaslužuje istragu; trajni porast odgoda povezanih s kvotom može upućivati na zloupotrebu ili neprikladan plan.
Uvedite migraciju prije koda koji upisuje novi stupac. Token isporučite putem upravitelja tajni platforme za hosting, zatim pokrenite:
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan test
Nakon rotiranja tokena ažurirajte tajnu okruženja i ponovno pokrenite php artisan config:cache. Stara predmemorirana vrijednost nastavit će uzrokovati neuspjele autentifikacije čak i kada je temeljna varijabla okruženja ispravna.
Uobičajeni kvarovi koje prvo treba provjeriti
401ili403obično upućuje na nedostajući, opozvani ili pogrešno kopirani token usluge. Nemojte ga ponavljati u tijesnoj petlji.429bi trebao proizvesti registraciju na čekanju, a ne odbijanje. Pregledajte podatke o kvoti i obrasce prometa prije promjene plana.- Ponovljena prekoračenja vremena čekanja mogu upućivati na probleme s DNS-om, vatrozidom, odlaznim HTTPS-om ili proxyjem. Tijekom istrage zadržite ograničeno vrijeme čekanja.
- Nepodudaranje ugovora znači da tijelu nedostaje obavezno polje najviše razine ili sadrži neočekivani tip. Zabilježite događaj, a ne osjetljivo tijelo.
- Ako je svaki rezultat odgođen, usporedite dokumentirani rječnik
statusirecommendations lokalnim preslikavačem pravila.
Završni kontrolni popis za provjeru
- Token postoji samo u konfiguraciji temeljenoj na okruženju.
- Zahtjev koristi točnu GET krajnju točku s URL-kodiranim parametrima
tokeniemail. - Vremenska ograničenja veze i ukupnog odgovora su ograničena.
- Samo neuspjele veze i privremeni kvarovi pristupnika dobivaju jedno ponavljanje.
- Nepoznati, neispravni, kvotom ograničeni i nedostupni odgovori odgađaju registraciju.
- Odbijanje se događa samo na temelju potpunog, prepoznatog odgovora.
- Logovi sadrže razloge i HTTP statusne kodove, ali ne tokene, adrese, URL-ove ni tijela.
- Funkcionalni testovi pokrivaju prihvaćanje, odbijanje i prekid rada usluge.
- Ručni test u staging okruženju potvrđuje da prekid rada API-ja i dalje stvara korisnika na čekanju.
Zrela integracija nije ona koja upućuje najviše API poziva. To je ona koja točno zna koliko povjerenja treba dati svakom odgovoru. Time što neizvjesnost postavlja kao prvoklasni domenski ishod, ovaj obrazac za registraciju dobiva korisnu zaštitu bez pretvaranja privremenih problema s ovisnom uslugom u zaključana ulazna vrata.