Pouzdana provjera e-pošte: omogućite neometane registracije tijekom prekida rada API-ja
Obrazac za registraciju ima dva zadatka koja se mogu kretati u suprotnim smjerovima: spriječiti nekvalitetne adrese i omogućiti pristup legitimnim osobama. Vanjska usluga za provjeru e-pošte poboljšava prvi zadatak, ali tretiranje te usluge kao nepogrešivog čuvara može neprimjetno sabotirati drugi. Istek vremena, potrošena kvota ili kratkotrajni uzvodni incident ne bi se trebali pretvoriti u „Registracija nije uspjela.”
Ovaj vodič izrađuje krajnju točku za registraciju u Native PHP 8.3 s namjerno blagom politikom. Lokalno neispravna sintaksa e-pošte odmah se odbija. Email Validator provjerava sintaksu, domenu, MX zapise, signale pružatelja usluge i praktični rizik isporuke. Njegov se odgovor mapira u tipizirani rezultat aplikacije. Međutim, privremeni API kvarovi nikad ne odbijaju podnositelja: račun se stvara kao nepotvrđen i nastavlja kroz uobičajenu potvrdu e-pošte.
Vanjski validator trebao bi ojačati odluke o registraciji, a ne postati jedina točka odbijanja.
Pribavite pristup prije pisanja integracijskog koda
Najprije se registrirajte za račun ili upotrijebite stranicu za prijavu 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.
- Pohranite token u konfiguraciju okruženja projekta, nikad u PHP izvornu kontrolu.
Ova usluga zahtijeva token. Autentikacija koristi parametar upita token={serviceToken}. Ponovno generiranje tokena usluge opoziva prethodno aktivni token, stoga rotacija tokena mora ažurirati implementirano okruženje prije nego što se očekuje rad starih vjerodajnica.
Potvrdite krajnju točku minimalnim zahtjevom
Točan poziv je GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Navedite i email i token kao parametre upita:
curl --silent --show-error \
--get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode '[email protected]' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN'
Pokrenite to samo u pouzdanoj ljusci. Budući da se vjerodajnica nalazi u nizu upita, povijest naredbi, izlaz za otklanjanje pogrešaka, zapisnici pristupa i zapisnici proxyja zaslužuju posebnu pozornost. Aplikacija u nastavku nikada ne bilježi URL zahtjeva.
Stvorite lokalnu datoteku .env i isključite je iz kontrole verzija:
APP_ENV=local
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
DB_DSN=sqlite:/absolute/path/to/project/var/app.sqlite
Za lokalni razvoj izvezite datoteku u procesno okruženje prije pokretanja PHP-a. Produkcija bi trebala ubrizgati iste varijable putem platforme za implementaciju, umjesto kopiranja .env u sliku:
set -a
. ./.env
set +a
php -S 127.0.0.1:8080 -t public
Arhitektura: propusti, ali provjeri prije povjerenja
„Propusti” ne znači tretirati svaku adresu kao pouzdanu. Znači omogućiti nastavak registracije, uz zadržavanje računa kao nepotvrđenog. Korisnik i dalje mora dovršiti tijek potvrde e-pošte aplikacije prije dobivanja privilegija koje zahtijevaju potvrđenu adresu.
Dizajn ima četiri granice:
- Kontroler provodi CSRF zaštitu, provjere obaveznih polja i nativnu provjeru sintakse.
- Namjenski cURL transport upravlja mrežnom mehanikom i ograničenim vremenskim istecima.
- Klijent Email Validator mapira udaljeni JSON u strukturirana stanja kao što su
checked,quota_limited,authentication_failureiunavailable. - Politika registracije odbija samo determinističke lokalne pogreške. Svaki udaljeni rezultat, uključujući uspješnu procjenu rizika, zadržava se kao kontekst za provjeru i vidljivost.
Dostavljeni ugovor navodi status, score, recommendation, checks i quota, ali ovdje ne utvrđuje njihove skupove vrijednosti ni ljestvicu bodovanja. Adapter stoga provjerava njihove tipove bez izmišljanja pragova ili značenja preporuka. Ako službena dokumentacija definira pravilo blokiranja koje želite primijeniti, kodirajte njegove točne dokumentirane vrijednosti u politiku i zadržite stanja prekida kao neblokirajuća.
Struktura projekta i ovisnosti
graceful-registration/
├── composer.json
├── .env
├── public/
│ └── register.php
├── src/
│ ├── EmailValidator.php
│ └── RegistrationPolicy.php
├── tests/
│ └── EmailValidatorClientTest.php
└── var/
Koristite Composer samo za automatsko učitavanje i PHPUnit. Mrežno povezivanje u vrijeme izvođenja ostaje nativni cURL:
{
"name": "example/graceful-registration",
"type": "project",
"require": {
"php": ">=8.3",
"ext-curl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.5"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
composer install
mkdir -p var
chmod 700 var
Izgradite obrambenu API granicu
Transport je zamjenjiv, čime testovi postaju deterministički. Klijent ponavlja pokušaj samo kod iznimke transporta, HTTP 408 ili odgovora poslužitelja 5xx, i izvodi najviše dva pokušaja. Neuspjesi autentikacije, neispravni zahtjevi i odgovori o kvoti ne pokušavaju se slijepo ponovo. Vremena povezivanja i ukupnog odgovora su ograničena.
<?php
// src/EmailValidator.php
declare(strict_types=1);
namespace App;
use JsonException;
use RuntimeException;
final readonly class HttpResponse
{
public function __construct(
public int $statusCode,
public string $body,
) {}
}
interface HttpTransport
{
public function get(
string $url,
int $connectTimeoutMs,
int $timeoutMs
): HttpResponse;
}
final class CurlTransport implements HttpTransport
{
public function get(
string $url,
int $connectTimeoutMs,
int $timeoutMs
): HttpResponse {
$handle = curl_init($url);
if ($handle === false) {
throw new RuntimeException('Unable to initialize cURL');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $timeoutMs,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($handle);
if ($body === false) {
$message = curl_error($handle);
curl_close($handle);
throw new RuntimeException('Email validator transport failed: ' . $message);
}
$statusCode = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($statusCode, $body);
}
}
final readonly class EmailValidationOutcome
{
public function __construct(
public string $state,
public ?string $status = null,
public ?float $score = null,
public ?string $recommendation = null,
public array $checks = [],
public array $quota = [],
) {}
}
final readonly class EmailValidatorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/email-validator/v1/check-email';
public function __construct(
private HttpTransport $transport,
private string $token,
private ?\Closure $sleep = null,
) {
if ($token === '' || $token === 'YOUR_SERVICE_TOKEN') {
throw new RuntimeException('EMAIL_VALIDATOR_TOKEN is not configured');
}
}
public function check(string $email): EmailValidationOutcome
{
$url = self::ENDPOINT . '?' . http_build_query(
['email' => $email, 'token' => $this->token],
'',
'&',
PHP_QUERY_RFC3986
);
for ($attempt = 1; $attempt <= 2; $attempt++) {
try {
$response = $this->transport->get($url, 2_000, 4_000);
} catch (RuntimeException) {
if ($attempt === 1) {
$this->backoff();
continue;
}
return new EmailValidationOutcome('unavailable');
}
$retryable = $response->statusCode === 408
|| $response->statusCode >= 500;
if ($retryable && $attempt === 1) {
$this->backoff();
continue;
}
return $this->map($response);
}
return new EmailValidationOutcome('unavailable');
}
private function map(HttpResponse $response): EmailValidationOutcome
{
if (in_array($response->statusCode, [401, 403], true)) {
return new EmailValidationOutcome('authentication_failure');
}
if ($response->statusCode === 429) {
return new EmailValidationOutcome('quota_limited');
}
if (in_array($response->statusCode, [400, 422], true)) {
return new EmailValidationOutcome('request_rejected');
}
if ($response->statusCode < 200 || $response->statusCode >= 300) {
return new EmailValidationOutcome('unavailable');
}
try {
$body = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException) {
return new EmailValidationOutcome('malformed_response');
}
if (!is_array($body)) {
return new EmailValidationOutcome('malformed_response');
}
$data = isset($body['data']) && is_array($body['data'])
? $body['data']
: $body;
$status = $body['status'] ?? $data['status'] ?? null;
$score = $data['score'] ?? null;
$recommendation = $data['recommendation'] ?? null;
$checks = $data['checks'] ?? null;
$quota = $body['quota'] ?? $data['quota'] ?? null;
if (
!is_string($status)
|| $status === ''
|| !is_int($score) && !is_float($score)
|| !is_string($recommendation)
|| $recommendation === ''
|| !is_array($checks)
|| !is_array($quota)
) {
return new EmailValidationOutcome('malformed_response');
}
return new EmailValidationOutcome(
state: 'checked',
status: $status,
score: (float) $score,
recommendation: $recommendation,
checks: $checks,
quota: $quota,
);
}
private function backoff(): void
{
$delay = 150_000 + random_int(0, 50_000);
$sleeper = $this->sleep
?? static fn (int $microseconds) => usleep($microseconds);
$sleeper($delay);
}
}
Adapter prihvaća obavezna polja u korijenu odgovora ili unutar objekta data, a zatim odbija nepotpune ili pogrešno tipizirane podatke kao neispravne. To je jačanje granice, a ne tvrdnja da su nedokumentirani oblici odgovora zajamčeni.
Pretvorite validaciju u odluku o registraciji
Politika drži lokalnu sigurnost odvojeno od udaljenih dokaza. Svi prihvaćeni računi zahtijevaju potvrdu e-pošte. Uspješan odgovor validatora bilježi njegov potpuni ugovor; prekid bilježi degradirano stanje bez gubitka registracije.
<?php
// src/RegistrationPolicy.php
declare(strict_types=1);
namespace App;
final readonly class RegistrationDecision
{
public function __construct(
public bool $accept,
public string $validatorState,
public ?EmailValidationOutcome $outcome = null,
public ?string $reason = null,
) {}
}
final readonly class RegistrationPolicy
{
public function __construct(private EmailValidatorClient $validator) {}
public function decide(string $email): RegistrationDecision
{
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
return new RegistrationDecision(
accept: false,
validatorState: 'local_rejection',
reason: 'Enter a syntactically valid email address.'
);
}
$outcome = $this->validator->check($email);
return new RegistrationDecision(
accept: true,
validatorState: $outcome->state,
outcome: $outcome
);
}
}
Povežite politiku s obrascem za registraciju
Kontroler stvara SQLite shemu za ovaj kompaktni projekt, provjerava CSRF stanje, hashira lozinku i pohranjuje račun kao nepotvrđen. Jedinstveno ograničenje sprječava dvostruke adrese. U većoj aplikaciji migracije bi trebale upravljati shemom, a postojeća komponenta za poštu trebala bi poslati jednokratnu poveznicu za potvrdu nakon potvrde transakcije.
<?php
// public/register.php
declare(strict_types=1);
use App\CurlTransport;
use App\EmailValidatorClient;
use App\RegistrationPolicy;
require dirname(__DIR__) . '/vendor/autoload.php';
session_start();
$token = getenv('EMAIL_VALIDATOR_TOKEN');
$dsn = getenv('DB_DSN');
if (!is_string($token) || !is_string($dsn) || $dsn === '') {
http_response_code(500);
exit('Application configuration is incomplete.');
}
$pdo = new PDO($dsn, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
email_verified INTEGER NOT NULL DEFAULT 0,
validator_state TEXT NOT NULL,
validator_status TEXT NULL,
validator_score REAL NULL,
validator_recommendation TEXT NULL,
created_at TEXT NOT NULL
)'
);
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo '<form method="post">
<input type="hidden" name="csrf" value="' .
htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8') . '">
<label>Email <input type="email" name="email" required></label>
<label>Password <input type="password" name="password"
minlength="12" required></label>
<button type="submit">Create account</button>
</form>';
exit;
}
$submittedCsrf = $_POST['csrf'] ?? '';
if (!is_string($submittedCsrf)
|| !hash_equals($_SESSION['csrf'], $submittedCsrf)
) {
http_response_code(403);
exit('Invalid form session.');
}
$email = trim((string) ($_POST['email'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
if (strlen($email) > 254 || strlen($password) < 12) {
http_response_code(422);
exit('Check the submitted email and password.');
}
$client = new EmailValidatorClient(new CurlTransport(), $token);
$decision = (new RegistrationPolicy($client))->decide($email);
if (!$decision->accept) {
http_response_code(422);
exit($decision->reason ?? 'Email cannot be accepted.');
}
$outcome = $decision->outcome;
try {
$statement = $pdo->prepare(
'INSERT INTO users (
email, password_hash, email_verified, validator_state,
validator_status, validator_score,
validator_recommendation, created_at
) VALUES (
:email, :password_hash, 0, :validator_state,
:validator_status, :validator_score,
:validator_recommendation, :created_at
)'
);
$statement->execute([
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'validator_state' => $decision->validatorState,
'validator_status' => $outcome?->status,
'validator_score' => $outcome?->score,
'validator_recommendation' => $outcome?->recommendation,
'created_at' => gmdate(DATE_ATOM),
]);
} catch (PDOException $exception) {
if ((string) $exception->getCode() === '23000') {
http_response_code(409);
exit('An account for that email already exists.');
}
throw $exception;
}
error_log(json_encode([
'event' => 'registration_created',
'validator_state' => $decision->validatorState,
'remote_status_present' => $outcome?->status !== null,
'score_present' => $outcome?->score !== null,
'recommendation_present' => $outcome?->recommendation !== null,
'checks_present' => ($outcome?->checks ?? []) !== [],
'quota_present' => ($outcome?->quota ?? []) !== [],
], JSON_THROW_ON_ERROR));
unset($_SESSION['csrf']);
http_response_code(201);
echo 'Account created. Check your inbox to verify your email address.';
Zapisnik koristi svako područje udaljenog ugovora za operativnu klasifikaciju bez bilježenja e-pošte, tokena, sirovih provjera ili sadržaja kvote. Pohranjivanje samo polja potrebnih politici proizvoda također smanjuje nepotrebno zadržavanje podataka.
Deterministički testirajte ponovne pokušaje i degradiranu registraciju
Lažni transport omogućuje testovima upravljanje putanjama uspjeha i neuspjeha bez mrežnog pristupa. Vrijednosti fixturea u nastavku namjerno su neprozirne; test provjerava mapiranje granice, a ne tvrdi semantiku specifičnu za uslugu.
<?php
// tests/EmailValidatorClientTest.php
declare(strict_types=1);
use App\EmailValidatorClient;
use App\HttpResponse;
use App\HttpTransport;
use PHPUnit\Framework\TestCase;
final class QueueTransport implements HttpTransport
{
public int $calls = 0;
public function __construct(private array $items) {}
public function get(
string $url,
int $connectTimeoutMs,
int $timeoutMs
): HttpResponse {
$this->calls++;
$item = array_shift($this->items);
if ($item instanceof Throwable) {
throw $item;
}
return $item;
}
}
final class EmailValidatorClientTest extends TestCase
{
public function testItRetriesOneServerFailureAndMapsTheResponse(): void
{
$transport = new QueueTransport([
new HttpResponse(503, ''),
new HttpResponse(200, json_encode([
'status' => 'fixture-status',
'score' => 12.5,
'recommendation' => 'fixture-recommendation',
'checks' => ['fixture-check' => true],
'quota' => ['fixture-quota' => 7],
], JSON_THROW_ON_ERROR)),
]);
$client = new EmailValidatorClient(
$transport,
'test-token',
static fn (int $microseconds) => null
);
$outcome = $client->check('[email protected]');
self::assertSame('checked', $outcome->state);
self::assertSame(12.5, $outcome->score);
self::assertSame(2, $transport->calls);
}
public function testAnOutageBecomesUnavailableAfterTwoAttempts(): void
{
$transport = new QueueTransport([
new RuntimeException('timeout'),
new RuntimeException('timeout'),
]);
$client = new EmailValidatorClient(
$transport,
'test-token',
static fn (int $microseconds) => null
);
self::assertSame(
'unavailable',
$client->check('[email protected]')->state
);
self::assertSame(2, $transport->calls);
}
public function testQuotaResponseIsNotRetried(): void
{
$transport = new QueueTransport([new HttpResponse(429, '')]);
$client = new EmailValidatorClient($transport, 'test-token');
self::assertSame(
'quota_limited',
$client->check('[email protected]')->state
);
self::assertSame(1, $transport->calls);
}
}
vendor/bin/phpunit --testdox tests
Dodajte testove na razini kontrolera za neispravne CSRF tokene, neispravne adrese, dvostruke račune, kratke lozinke, neuspjeh autentikacije, neispravan JSON i uspješnu registraciju tijekom simuliranog isteka vremena. Ključna je tvrdnja da nedostupan validator i dalje proizvodi nepotvrđeni korisnički zapis.
Sigurnost, vidljivost i implementacija
Tretirajte token usluge kao tajnu iako putuje u parametru upita. Držite ga izvan repozitorija, poruka iznimki, tragova performansi aplikacije, zapisnika upita obrnutog proxyja i kopiranih URL-ova zahtjeva. Ograničite pristup konfiguraciji implementacije i namjerno rotirajte token, imajući na umu da ponovno generiranje opoziva stari token.
Ograničite brzinu rute za registraciju prema IP-u i širim signalima zloupotrebe, ali izbjegavajte oslanjanje samo na IP. Zadržite CSRF zaštitu, hashiranje lozinki, istek tokena potvrde, obradu duplikata i generičke odgovore o računu. Validator nadopunjuje ove kontrole; ne zamjenjuje ih.
Pratite brojanja prema validator_state, učestalost ponovnih pokušaja, latenciju, neispravne odgovore i prisutnost podataka o kvoti. Upozorite na trajni authentication_failure, jer to obično zahtijeva radnju operatera. Porast poziva quota_limited sugerira pregled plana ili prometa. Kratki nalet stanja unavailable trebao bi degradirati registraciju, a ne korisnicima prikazivati uzvodnu pogrešku.
Tijekom implementacije provjerite jesu li cURL i PDO SQLite omogućeni, može li direktorij baze podataka zapisivati samo račun aplikacije, je li Composerov optimizirani autoloader izgrađen i sadrži li okruženje aktivni token. Implementirajte kod koji razumije i stara i nova operativna stanja prije rotacije vjerodajnica. Za više instanci aplikacije zamijenite SQLite zajedničkom bazom podataka aplikacije, zadržavajući istu politiku i jedinstveno ograničenje.
Uobičajeni načini neuspjeha
- Svaki poziv vraća neuspjeh autentikacije: potvrdite token ograničen na uslugu, aktivaciju plana, ubrizgavanje u okruženje i je li netko ponovno generirao token.
- Odgovori o kvoti pokreću spore obrasce: ne ponavljajte HTTP 429 unutar zahtjeva. Zabilježite stanje i nastavite s potvrdom e-pošte.
- Istek vremena troši skup PHP radnika: zadržite kratka vremena isteka povezivanja i ukupna vremena isteka te se oduprite umnožavanju ponovnih pokušaja.
- Uspješni odgovori postaju neispravni: pregledajte sigurno redigirani odgovor u odnosu na službenu dokumentaciju. Nemojte tiho prisilno pretvarati nedostajuća polja.
- Korisnici su i dalje isključeni tijekom incidenata: provjerite politiku kontrolera. U ovom dizajnu trebala bi odbijati samo deterministička lokalna validacija.
- Tokeni se pojavljuju u zapisnicima: onemogućite bilježenje niza upita za ovo odlazno odredište i uklonite vrijednosti URL-a iz iznimki i atributa praćenja.
Završni kontrolni popis za provjeru
- Račun i Free, Plus ili Pro plan su aktivni.
- Token usluge dolazi s ploče Service token na stranici dokumentacije.
EMAIL_VALIDATOR_TOKENubrizgava se kroz konfiguraciju okruženja.- Zahtjev koristi GET, točnu krajnju točku te parametre upita
emailitoken. - Vremena isteka povezivanja i odgovora su ograničena.
- Samo prolazni kvarovi transporta, 408 i 5xx dobivaju jedan ponovni pokušaj.
- Neuspjesi autentikacije, validacije zahtjeva i kvote ne ponavljaju se.
status,score,recommendation,checksiquotaprovjeravaju se prema tipu na granici.- Privremeni API kvarovi i dalje stvaraju nepotvrđeni račun.
- Zapisnici sadrže strukturirana stanja, ali ne i adresu e-pošte, sirovi URL ili token.
- Automatizirani testovi pokrivaju uspjeh, iscrpljivanje ponovnih pokušaja i obradu kvote.
- Uobičajeni tijek potvrde e-pošte ostaje obavezan prije dodjele povjerenja.
Najotporniji sustav registracije nije onaj koji se pretvara da ovisnosti nikada ne otkazuju. To je onaj koji razlikuje sigurnost od dokaza: odbija ono za što aplikacija može dokazati da je neispravno, obogaćuje odluke kada validator odgovori i čuva siguran put naprijed kada ne odgovori. To održava zaštitu snažnom bez pretvaranja dostupnosti u tuđe obećanje.