Laravel registracija: zadržite korisnike u procesu prijave kada provjera e-pošte zakaže
Obrazac za registraciju ima jedan zadatak: omogućiti legitimnim osobama da izrade račun. Provjera e-pošte poboljšava taj tijek filtriranjem očitih pogrešaka i visokorizičnih adresa, ali postaje kontraproduktivna kada privremeni prekid ovisne usluge svima onemogući pristup.
Ovaj vodič izrađuje produkcijski orijentiranu Laravel integraciju koja putem Email Validator API-ja provjerava sintaksu, domenu, MX zapise, signale pružatelja usluge i praktični rizik isporuke. Preporuke za konačno odbijanje zaustavljaju registraciju. Istekla vremena, iscrpljena kvota, ograničenja brzine, neispravni odgovori i privremeni kvarovi poslužitelja stvaraju strukturirani rezultat „nedostupno” i omogućuju nastavak registracije.
Dizajn je namjerno sinkron jer rezultat utječe na trenutačnu predaju obrasca. Također je namjerno otvoren pri neuspjesima u radu sustava. Laravelov uobičajeni tijek provjere e-pošte treba ostati konačni dokaz da podnositelj prijave upravlja adresom.
Preduvjeti i pristup usluzi
Potrebni su vam PHP 8.3 ili noviji, aktualna Laravel aplikacija, Composer, konfigurirana baza podataka te Laravelov standardni model User i obrazac za registraciju. Primjeri koriste Laravelov ugrađeni HTTP klijent, provjeru valjanosti, zapisivanje, ograničavanje brzine i testne zamjene; nije potreban dodatni HTTP paket.
Pribavite pristup usluzi prije pisanja integracije:
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- 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 tu uslugu.
Ova usluga zahtijeva token. Njegova regeneracija opoziva prethodno aktivni token, stoga uskladite rotaciju s implementacijom: ažurirajte tajnu varijablu okruženja, ponovno implementirajte ili učitajte konfiguraciju, potvrdite novi token i tek tada smatrajte promjenu dovršenom.
Potvrdite točan zahtjev
API poziv je GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Autentikacija koristi parametar upita token, dok se adresa šalje u parametru email. Uputite jedan minimalan zahtjev s neprodukcijskom testnom adresom:
curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN' \
--data-urlencode '[email protected]' \
--header 'Accept: application/json'
Vjerodajnica u nizu upita može se pojaviti u povijesti ljuske i zapisima pristupa infrastrukturi. Naredbu koristite samo u kontroliranom okruženju, u zajedničkoj dokumentaciji zadržite rezervirano mjesto te konfigurirajte proxyje i zapisnike aplikacije da prikrivaju nizove upita.
Stvarni token pohranite u Laravelovu konfiguraciju okruženja, nikada u izvornu kontrolu:
# .env
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_URL=https://ai.mihajlo.mk/api/email-validator/v1/check-email
# These are application policy values. Match them to the recommendation
# vocabulary documented for your activated service.
EMAIL_VALIDATOR_SUCCESS_STATUSES=success
EMAIL_VALIDATOR_ALLOW_RECOMMENDATIONS=accept
EMAIL_VALIDATOR_REJECT_RECOMMENDATIONS=reject
Dodajte odgovarajuću konfiguraciju u config/services.php. Čitanje varijabli okruženja ovdje održava kôd aplikacije kompatibilnim s php artisan config:cache.
<?php
return [
// Existing services...
'email_validator' => [
'url' => env(
'EMAIL_VALIDATOR_URL',
'https://ai.mihajlo.mk/api/email-validator/v1/check-email'
),
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'success_statuses' => array_filter(array_map(
'trim',
explode(',', env('EMAIL_VALIDATOR_SUCCESS_STATUSES', 'success'))
)),
'allow_recommendations' => array_filter(array_map(
'trim',
explode(',', env('EMAIL_VALIDATOR_ALLOW_RECOMMENDATIONS', 'accept'))
)),
'reject_recommendations' => array_filter(array_map(
'trim',
explode(',', env('EMAIL_VALIDATOR_REJECT_RECOMMENDATIONS', 'reject'))
)),
],
];
Konfigurabilni rječnik je važan. Isporučeni ugovor navodi status, score, recommendation, checks i quota, ali aplikacijski kôd ne bi trebao nagađati nedokumentirana značenja ili pragove rezultata. Potvrdite trenutačno dokumentirane vrijednosti preporuka i konfigurirajte popise u skladu s njima.
Arhitektura: autoritativno odbijanje, postupna degradacija
Kontroler registracije najprije provodi jeftinu lokalnu provjeru valjanosti. Namjenska API granica zatim prevodi udaljeni odgovor u jedan od tri ishoda domene:
- Dopusti: odgovor je potpun i njegova preporuka odgovara konfiguriranom popisu dopuštenih vrijednosti.
- Odbij: odgovor je potpun i njegova preporuka odgovara konfiguriranom popisu odbijenih vrijednosti.
- Nedostupno: problemi s prijenosom, autentikacijom, kvotom, shemom, statusom ili nepoznatom preporukom sprječavaju pouzdanu odluku.
Samo Reject zaustavlja registraciju. Unavailable se zapisuje i otvoreno propušta. Time se problemi infrastrukture razlikuju od dokaza da adresu treba odbiti.
Mapirajte API odgovor na jednoj granici
Izradite app/Services/EmailValidation/EmailAssessment.php:
<?php
namespace App\Services\EmailValidation;
enum AssessmentVerdict: string
{
case Allow = 'allow';
case Reject = 'reject';
case Unavailable = 'unavailable';
}
final readonly class EmailAssessment
{
public function __construct(
public AssessmentVerdict $verdict,
public ?float $score = null,
public ?string $recommendation = null,
public array $checks = [],
public array $quota = [],
public ?string $failure = null,
) {}
public static function unavailable(string $failure): self
{
return new self(AssessmentVerdict::Unavailable, failure: $failure);
}
}
Sada izradite app/Services/EmailValidation/EmailValidatorClient.php. Koristi kratka, ograničena vremenska ograničenja i ponovno pokušava samo neuspjele veze i odgovore poslužitelja 5xx. Ne pokušava naslijepo ponovno autentikacijske neuspjehe, nevaljane zahtjeve ni HTTP 429 odgovore.
<?php
namespace App\Services\EmailValidation;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
final class EmailValidatorClient
{
public function check(string $email): EmailAssessment
{
$token = config('services.email_validator.token');
if (! is_string($token) || $token === '') {
Log::critical('email_validator.missing_token');
return EmailAssessment::unavailable('configuration');
}
try {
$response = Http::acceptJson()
->connectTimeout(2)
->timeout(5)
->retry(
3,
200,
function (Exception $exception): bool {
return $exception instanceof ConnectionException
|| ($exception instanceof RequestException
&& $exception->response->serverError());
},
throw: false
)
->get(config('services.email_validator.url'), [
'token' => $token,
'email' => $email,
]);
} catch (ConnectionException $exception) {
Log::warning('email_validator.connection_failed', [
'exception' => $exception::class,
]);
return EmailAssessment::unavailable('connection');
} catch (Throwable $exception) {
report($exception);
return EmailAssessment::unavailable('client_exception');
}
if ($response->status() === 429) {
return EmailAssessment::unavailable('rate_limited');
}
if (in_array($response->status(), [401, 403], true)) {
Log::critical('email_validator.authentication_failed');
return EmailAssessment::unavailable('authentication');
}
if (! $response->successful()) {
Log::warning('email_validator.http_failure', [
'http_status' => $response->status(),
]);
return EmailAssessment::unavailable('http_failure');
}
$payload = $response->json();
if (! is_array($payload)) {
return EmailAssessment::unavailable('invalid_json');
}
// Defensively support either direct fields or a conventional data envelope.
$source = isset($payload['data']) && is_array($payload['data'])
? $payload['data']
: $payload;
$status = $source['status'] ?? $payload['status'] ?? null;
$score = $source['score'] ?? null;
$recommendation = $source['recommendation'] ?? null;
$checks = $source['checks'] ?? null;
$quota = $source['quota'] ?? $payload['quota'] ?? null;
if (! is_string($status)
|| ! is_numeric($score)
|| ! is_string($recommendation)
|| ! is_array($checks)
|| $checks === []
|| ! is_array($quota)) {
return EmailAssessment::unavailable('invalid_schema');
}
$status = strtolower(trim($status));
$recommendation = strtolower(trim($recommendation));
$successStatuses = array_map(
'strtolower',
config('services.email_validator.success_statuses', [])
);
if (! in_array($status, $successStatuses, true)) {
return EmailAssessment::unavailable('service_status');
}
$remaining = $this->findRemainingQuota($quota);
if ($remaining !== null && $remaining <= 0) {
return EmailAssessment::unavailable('quota_exhausted');
}
$reject = array_map(
'strtolower',
config('services.email_validator.reject_recommendations', [])
);
$allow = array_map(
'strtolower',
config('services.email_validator.allow_recommendations', [])
);
$verdict = match (true) {
in_array($recommendation, $reject, true) => AssessmentVerdict::Reject,
in_array($recommendation, $allow, true) => AssessmentVerdict::Allow,
default => AssessmentVerdict::Unavailable,
};
Log::info('email_validator.assessed', [
'email_hash' => hash_hmac('sha256', strtolower($email), config('app.key')),
'verdict' => $verdict->value,
'status' => $status,
'score' => (float) $score,
'recommendation' => $recommendation,
'check_count' => count($checks),
'quota_remaining' => $remaining,
]);
return new EmailAssessment(
$verdict,
(float) $score,
$recommendation,
$checks,
$quota,
$verdict === AssessmentVerdict::Unavailable
? 'unknown_recommendation'
: null,
);
}
private function findRemainingQuota(array $quota): ?float
{
foreach ($quota as $key => $value) {
if (strtolower((string) $key) === 'remaining' && is_numeric($value)) {
return (float) $value;
}
if (is_array($value)) {
$remaining = $this->findRemainingQuota($value);
if ($remaining !== null) {
return $remaining;
}
}
}
return null;
}
}
Svih pet komponenti odgovora sudjeluje u pouzdanosti: status mora signalizirati uspjeh, score mora biti numerički, recommendation određuje rezultat politike, checks mora sadržavati strukturirane dokaze, a quota se pregledava radi iscrpljenosti. Aplikacija izbjegava izmišljanje praga rezultata jer se nikakva ljestvica ni prag ne smiju pretpostaviti bez izričitog ugovora usluge.
Povežite odluku s registracijom
Izradite ili ažurirajte app/Http/Controllers/RegisteredUserController.php:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Services\EmailValidation\AssessmentVerdict;
use App\Services\EmailValidation\EmailValidatorClient;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
final class RegisteredUserController extends Controller
{
public function store(
Request $request,
EmailValidatorClient $emailValidator
): RedirectResponse {
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'confirmed', 'min:12'],
]);
$assessment = $emailValidator->check($validated['email']);
if ($assessment->verdict === AssessmentVerdict::Reject) {
return back()
->withInput($request->except('password', 'password_confirmation'))
->withErrors([
'email' => 'Please use a different email address.',
]);
}
if ($assessment->verdict === AssessmentVerdict::Unavailable) {
Log::notice('registration.email_validation_bypassed', [
'reason' => $assessment->failure,
]);
}
$user = DB::transaction(function () use ($validated): User {
$user = new User();
$user->name = $validated['name'];
$user->email = $validated['email'];
$user->password = Hash::make($validated['password']);
$user->save();
return $user;
});
event(new Registered($user));
Auth::login($user);
return redirect()->route('dashboard');
}
}
Ostavite email_verified_at nepostavljenim i zahtijevajte Laravelovu provjeru e-pošte prije osjetljivih radnji. API provjera procjenjuje kvalitetu adrese; ne dokazuje vlasništvo.
Zaštitite kvotu od zlonamjernih predaja
Registrirajte imenovani limiter u app/Providers/AppServiceProvider.php, zatim ga pridružite ruti registracije:
<?php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
RateLimiter::for('registration', function (Request $request): array {
$emailKey = hash(
'sha256',
strtolower(trim((string) $request->input('email')))
);
return [
Limit::perMinute(5)->by($request->ip()),
Limit::perHour(10)->by($request->ip().'|'.$emailKey),
];
});
// routes/web.php
Route::middleware(['guest', 'throttle:registration'])
->post('/register', [RegisteredUserController::class, 'store']);
Ograničavanje brzine pripada prije plaćenog poziva ili poziva ograničenog kvotom. Laravelov CSRF middleware neka ostane omogućen, provedite lokalnu provjeru valjanosti prije pozivanja usluge i nikada u zapisnike ne uključujte token, puni URL zahtjeva, neobrađenu e-poštu ni potpuni udaljeni odgovor.
Deterministički testirajte putanje odbijanja i neuspjeha
Http::fake() sprječava testove da troše kvotu ili ovise o mreži. Nizovi preporuka u donjoj testnoj pripremi predstavljaju konfiguriranu politiku aplikacije; promijenite ih zajedno ako se dokumentirani rječnik usluge razlikuje.
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\EmailValidation\AssessmentVerdict;
use App\Services\EmailValidation\EmailValidatorClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class RegistrationEmailValidationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
config([
'services.email_validator.token' => 'test-token',
'services.email_validator.success_statuses' => ['success'],
'services.email_validator.allow_recommendations' => ['accept'],
'services.email_validator.reject_recommendations' => ['reject'],
]);
}
public function test_client_maps_a_rejection(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*' =>
Http::response([
'status' => 'success',
'score' => 12,
'recommendation' => 'reject',
'checks' => ['syntax' => true, 'mx' => false],
'quota' => ['remaining' => 99],
], 200),
]);
$result = app(EmailValidatorClient::class)
->check('[email protected]');
$this->assertSame(AssessmentVerdict::Reject, $result->verdict);
}
public function test_temporary_failure_does_not_reject_registration(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*' =>
Http::response([], 503),
]);
$response = $this->post('/register', [
'name' => 'Casey',
'email' => '[email protected]',
'password' => 'a-long-test-password',
'password_confirmation' => 'a-long-test-password',
]);
$response->assertRedirect(route('dashboard'));
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
]);
Http::assertSentCount(3);
}
public function test_rate_limit_is_not_retried(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*' =>
Http::response([], 429),
]);
$result = app(EmailValidatorClient::class)
->check('[email protected]');
$this->assertSame(AssessmentVerdict::Unavailable, $result->verdict);
Http::assertSentCount(1);
}
}
Dodajte još jedan funkcionalni test koji potvrđuje da konfigurirano odbijanje ponovno prikazuje obrazac, čuva unos koji nije tajan, ne stvara korisnika i nikada napadaču ne vraća interni razlog pružatelja usluge.
Implementacija i nadziranost
Postavite token putem spremišta tajni svoje platforme za implementaciju, provjerite sadrži li produkcija predviđeni rječnik preporuka i ponovno izgradite Laravelove predmemorije:
php artisan test
php artisan config:clear
php artisan config:cache
php artisan route:cache
Ne stavljajte aktivan API zahtjev u krajnju točku zdravstvenog stanja aplikacije; česte provjere mogu trošiti kvotu i učiniti zdravlje implementacije ovisnim o neobaveznoj vanjskoj usluzi. Umjesto toga upotrijebite kontroliranu probnu registraciju u staging okruženju.
Postavite upozorenja za trajni rast u registration.email_validation_bypassed, autentikacijske neuspjehe, ograničenja brzine, iscrpljenost kvote i nepoznate preporuke. Kratak neuspjeh veze uobičajena je degradacija. Trajna autentikacijska pogreška obično znači istekao ili regeneriran token, dok nepoznata preporuka često signalizira promjenu ugovora ili konfiguracije.
Uobičajeni načini neuspjeha
- Svaki zahtjev je nedostupan: provjerite predmemoriranu konfiguraciju, aktivaciju tokena, pravopis krajnje točke, izlazni HTTPS pristup i konfigurirani status uspjeha.
- Regenerirani token i dalje ne radi: zamijenite tajnu u svakoj pokrenutoj instanci te ponovno izgradite ili ponovno pokrenite predmemoriranu konfiguraciju.
- Broj HTTP 429 odgovora raste: pregledajte kontrole zlouporabe i plan kvote. Ne pojačavajte problem ponovnim pokušajima.
- Odgovori koji izgledaju valjano mapiraju se na nedostupno: usporedite stvarni dokumentirani oblik odgovora i rječnik preporuka s graničnim mapperom. Nepoznate vrijednosti zadržite otvorenima pri neuspjehu i vidljivima.
- Latencija registracije raste: pregledajte vrijeme povezivanja i ponovne pokušaje za 5xx. Sačuvajte stroga ograničenja vremena umjesto da čekate neograničeno.
Završni kontrolni popis za provjeru
- Točna GET krajnja točka prima samo parametre upita
tokeniemail. - Token usluge postoji samo u tajnoj konfiguraciji podržanoj varijablama okruženja.
- Lokalna provjera valjanosti i ograničavanje brzine izvršavaju se prije vanjskog zahtjeva.
- Status, rezultat, preporuka, provjere i kvota provjeravaju se na API granici.
- Samo konfigurirane, autoritativne preporuke odbijaju adresu.
- Istekla vremena, odgovori 429, iscrpljena kvota, neispravni sadržaji i privremeni 5xx neuspjesi omogućuju nastavak registracije.
- Zapisnici sadrže strukturirane operativne podatke, ali ne token ni neobrađenu adresu e-pošte.
- Provjera vlasništva e-pošte i dalje štiti osjetljive značajke aplikacije.
- Automatizirani testovi pokrivaju odbijanje, iscrpljivanje ponovnih pokušaja i ograničavanje brzine bez ponovnog pokušaja.
Najpouzdanija prepreka pri registraciji nije ona koja se pretvara da ovisnosti nikada ne otkazuju. To je ona koja zna razliku između loše adrese i lošeg poslijepodneva na mreži. Sačuvajte tu razliku i provjera e-pošte ojačat će ulazna vrata bez pretvaranja u bravu koja legitimne korisnike ostavlja vani.