Vodiči

Laravel Registration: Keep Users Signing Up When Email Validation Falters

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:

  1. Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
  2. Otvorite stranicu usluge Email Validator.
  3. Odaberite dostupni Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
  4. Otvorite službenu dokumentaciju za Email Validator.
  5. 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 token i email.
  • 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.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.