Laravel: Безбедни регистрации со резервни опции за валидација на е-пошта со ВИ
Валидацијата на е-пошта станува опасна кога се третира како едноставен мрежен повик со одговор да или не. Отфрлете ја секоја сомнителна адреса и ќе ги намалите регистрациите со низок квалитет. Но, ако го отфрлите секое барање кога услугата за валидација не е достапна, краток истек на време може да ги оневозможи легитимните корисници да пристапат до вашата апликација.
Овој туторијал создава продукциски ориентиран Laravel тек за регистрација што проверува синтакса, информации за доменот, MX записи, сигнали од давателот и практичен ризик од испорака преку Email Validator API. Јасните негативни препораки се отфрлаат, успешните проценки се евидентираат, а привремените неуспеси создаваат одложен резултат наместо нефункционална страница за регистрација.
Добијте пристап и копирајте го токенот за услугата
Започнете со создавање сметка на https://ai.mihajlo.mk/register. Ако веќе имате сметка, најавете се на https://ai.mihajlo.mk/login.
- Отворете ја страницата на услугата Email Validator.
- Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за Email Validator.
- Пронајдете го панелот Service token и копирајте го токенот ограничен на услугата.
- Чувајте го токенот во конфигурација поддржана од околината, никогаш во PHP изворен код или контрола на верзии.
Оваа услуга бара токен. Неговото повторно генерирање го поништува претходно активниот токен, па ротацијата на токени мора да вклучува ажурирање на секоја распоредена околина што ја користи старата вредност.
Точната API операција е GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Автентикацијата го користи параметарот за пребарување token, додека адресата се доставува преку email. Пред да напишете Laravel код, направете едно минимално барање од безбеден терминал:
curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN' \
--data-urlencode '[email protected]'
Успешен одговор обезбедува status, score, recommendation, checks и quota. Не печатете продукциски одговори во споделени логови бидејќи може да содржат податоци поврзани со адреси.
Додајте го акредитивот во Laravel околината пред да ја изградите функционалноста:
# .env
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_CONNECT_TIMEOUT=2
EMAIL_VALIDATOR_TIMEOUT=5
EMAIL_VALIDATOR_SUCCESS_STATUS=success
EMAIL_VALIDATOR_BLOCK_RECOMMENDATIONS=reject
Поставките за статус и препорака се намерно конфигурација на апликацијата. Потврдете ги нивните точни вредности според официјалната документација и одговорите достапни за вашиот активиран план. Интеграцијата не претпоставува недокументирани вгнездени полиња во checks или quota.
Архитектура и структура на проектот
Оваа имплементација е наменета за PHP 8.3 или понова верзија и тековна Laravel апликација со нејзината вообичаена табела users, инфраструктура за автентикација, база на податоци и тест-околина. Таа го користи вградениот HTTP клиент на Laravel, па не е потребен дополнителен HTTP пакет.
Релевантните датотеки се:
config/services.phpза поставки поддржани од околинатаapp/Domain/EmailValidation/EmailAssessment.phpза доменскиот резултатapp/Services/EmailValidator.phpза HTTP транспорт, мапирање на одговор и политикаapp/Http/Controllers/Auth/RegisteredUserController.phpза регистрацијаdatabase/migrations/..._add_email_validation_state_to_users_table.phpза зачувување на одложената состојбаtests/Feature/RegistrationEmailValidationTest.phpза детерминистички тестови на надворешни повици
Контролерот никогаш не расудува за суров JSON или HTTP статусни кодови. Тој прима еден од три исходи: дозволи, отфрли или одложи. Само отфрли го блокира формуларот. Мрежните неуспеси, одговорите за квота, неправилните payload-и и грешките на услугата стануваат одложи, овозможувајќи регистрација додека се зачувува оперативен сигнал за подоцнежен преглед.
Конфигурирајте ја API границата
Додајте посебен запис во config/services.php:
'email_validator' => [
'url' => 'https://ai.mihajlo.mk/api/email-validator/v1/check-email',
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'connect_timeout' => (int) env('EMAIL_VALIDATOR_CONNECT_TIMEOUT', 2),
'timeout' => (int) env('EMAIL_VALIDATOR_TIMEOUT', 5),
'success_status' => env('EMAIL_VALIDATOR_SUCCESS_STATUS', 'success'),
'block_recommendations' => array_values(array_filter(array_map(
'trim',
explode(',', env('EMAIL_VALIDATOR_BLOCK_RECOMMENDATIONS', 'reject'))
))),
],
Пристапот до околината останува во конфигурациските датотеки, што значи дека config:cache останува безбеден. Класите на апликацијата читаат config(), а не env().
Претставете ја доменската одлука
<?php
namespace App\Domain\EmailValidation;
enum EmailOutcome: string
{
case Allow = 'allow';
case Reject = 'reject';
case Defer = 'defer';
}
final readonly class EmailAssessment
{
public function __construct(
public EmailOutcome $outcome,
public string $reason,
public ?string $status = null,
public ?float $score = null,
public ?string $recommendation = null,
public array $checks = [],
public array $quota = [],
) {
}
public static function defer(string $reason): self
{
return new self(EmailOutcome::Defer, $reason);
}
}
Причината е стабилна внатрешна ознака, а не текст што директно му се прикажува на корисникот. Целосните оддалечени полиња остануваат достапни за доменскиот код, но на контролерот му е потребен само нормализираниот исход.
Изградете ја Laravel HTTP услугата
<?php
namespace App\Services;
use App\Domain\EmailValidation\EmailAssessment;
use App\Domain\EmailValidation\EmailOutcome;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
final class EmailValidator
{
public function check(string $email): EmailAssessment
{
$token = config('services.email_validator.token');
if (! is_string($token) || $token === '') {
Log::error('Email validator token is not configured');
return EmailAssessment::defer('configuration_error');
}
for ($attempt = 1; $attempt <= 2; $attempt++) {
try {
$response = Http::acceptJson()
->connectTimeout(config('services.email_validator.connect_timeout'))
->timeout(config('services.email_validator.timeout'))
->get(config('services.email_validator.url'), [
'token' => $token,
'email' => $email,
]);
} catch (ConnectionException $exception) {
Log::warning('Email validator connection failure', [
'attempt' => $attempt,
'exception' => $exception::class,
]);
if ($attempt === 1) {
usleep(200_000);
continue;
}
return EmailAssessment::defer('connection_failure');
}
if ($attempt === 1 && in_array($response->status(), [502, 503, 504], true)) {
usleep(200_000);
continue;
}
return $this->mapResponse($response);
}
return EmailAssessment::defer('connection_failure');
}
private function mapResponse(Response $response): EmailAssessment
{
if ($response->status() === 429) {
Log::warning('Email validator quota or rate limit reached');
return EmailAssessment::defer('quota_or_rate_limit');
}
if (in_array($response->status(), [401, 403], true)) {
Log::error('Email validator authentication failed', [
'http_status' => $response->status(),
]);
return EmailAssessment::defer('authentication_failure');
}
if (! $response->successful()) {
Log::warning('Email validator returned an HTTP failure', [
'http_status' => $response->status(),
]);
return EmailAssessment::defer('remote_http_failure');
}
$body = $response->json();
$required = ['status', 'score', 'recommendation', 'checks', 'quota'];
if (! is_array($body)
|| array_diff($required, array_keys($body)) !== []
|| ! is_string($body['status'])
|| ! is_numeric($body['score'])
|| ! is_string($body['recommendation'])
|| ! is_array($body['checks'])
|| ! is_array($body['quota'])
|| $body['checks'] === []
|| $body['quota'] === []) {
Log::warning('Email validator returned an invalid contract');
return EmailAssessment::defer('invalid_response');
}
$status = trim($body['status']);
$score = (float) $body['score'];
$recommendation = strtolower(trim($body['recommendation']));
$successStatus = strtolower((string) config(
'services.email_validator.success_status'
));
if (strtolower($status) !== $successStatus || ! is_finite($score)) {
return EmailAssessment::defer('remote_status_not_usable');
}
$blocked = array_map(
static fn (string $value): string => strtolower($value),
config('services.email_validator.block_recommendations', [])
);
$outcome = in_array($recommendation, $blocked, true)
? EmailOutcome::Reject
: EmailOutcome::Allow;
return new EmailAssessment(
outcome: $outcome,
reason: $outcome === EmailOutcome::Reject
? 'remote_recommendation_blocked'
: 'remote_recommendation_allowed',
status: $status,
score: $score,
recommendation: $recommendation,
checks: $body['checks'],
quota: $body['quota'],
);
}
}
Политиката за повторен обид е намерно тесна. Еден повторен обид покрива привремени неуспеси на врската и одговори од типот gateway 502, 503 или 504. Неуспесите при автентикација, неуспесите при валидација и одговорите за квота не се повторуваат слепо. Ограничените истеци на време за поврзување и за целосен одговор спречуваат бавна зависност да ги потроши сите достапни PHP работници.
Маперот го проверува секое договорено поле. status утврдува дали одговорот е употреблив, score мора да биде конечен, recommendation ја управува конфигурираната политика дозволи-или-отфрли, а непразните checks и quota потврдуваат дека пристигнале очекуваните контекст на проценка и користење. Не се претпоставува недокументирано име на проверка или подполе на квота.
Зачувајте ја одложената валидација без блокирање на регистрацијата
Додајте компактна колона за состојба во табелата users:
<?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_validation_state', 16)
->default('pending');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn('email_validation_state');
});
}
};
Извршете php artisan migrate. Состојбата поддржува вообичаено оперативно следење без воведување редица само за архитектонска декорација. Подоцнежен закажан процес за усогласување може повторно да ги разгледа одложените сметки ако производот го бара тоа.
Поврзете ја валидацијата со регистрацијата
Во контролерот за регистрација, прво извршете ја локалната валидација на Laravel. Така се избегнува трошење оддалечена квота за неправилно форматирани адреси, слаби лозинки или дупликати.
<?php
namespace App\Http\Controllers\Auth;
use App\Domain\EmailValidation\EmailOutcome;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\EmailValidator;
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\Validation\Rules\Password;
final class RegisteredUserController extends Controller
{
public function store(
Request $request,
EmailValidator $emailValidator
): RedirectResponse {
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
]);
$assessment = $emailValidator->check($validated['email']);
if ($assessment->outcome === EmailOutcome::Reject) {
return back()
->withInput($request->except('password', 'password_confirmation'))
->withErrors([
'email' => 'Please use an email address that can receive account messages.',
]);
}
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'email_validation_state' => $assessment->outcome->value,
]);
event(new Registered($user));
Auth::login($user);
return redirect()->route('dashboard');
}
}
Задржете ја рутата во routes/web.php, каде Laravel обезбедува заштита на сесијата и CSRF:
use App\Http\Controllers\Auth\RegisteredUserController;
use Illuminate\Support\Facades\Route;
Route::post('/register', [RegisteredUserController::class, 'store'])
->middleware('throttle:6,1')
->name('register');
Ограничувањето го намалува автоматизираното злоупотребување пред да може да троши квота за валидација. Правилото за е-пошта на Laravel останува неопходно бидејќи надворешната проценка на ризик треба да ја надополни, а не да ја замени, детерминистичката локална валидација на влезот.
Тестирајте ги одлуките и патеките на неуспех
Http::fake() ги одржува тестовите детерминистички и докажува дека не се потребни вистински акредитиви или мрежен пристап. Овие функционални тестови покриваат строга препорака и суштинското однесување со отворен неуспех.
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class RegistrationEmailValidationTest extends TestCase
{
use RefreshDatabase;
public function test_blocked_recommendation_rejects_registration(): void
{
Http::fake([
'*' => Http::response([
'status' => 'success',
'score' => 12,
'recommendation' => 'reject',
'checks' => ['available' => true],
'quota' => ['reported' => true],
]),
]);
$this->post('/register', $this->payload())
->assertSessionHasErrors('email');
$this->assertDatabaseMissing('users', [
'email' => '[email protected]',
]);
}
public function test_temporary_failures_defer_but_allow_registration(): void
{
Http::fakeSequence()
->push([], 503)
->push([], 503);
$this->post('/register', $this->payload())
->assertRedirect(route('dashboard'));
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
'email_validation_state' => 'defer',
]);
Http::assertSentCount(2);
}
private function payload(): array
{
return [
'name' => 'Example Person',
'email' => '[email protected]',
'password' => 'A-long-test-password',
'password_confirmation' => 'A-long-test-password',
];
}
}
Додајте тестови за дозволена препорака, неправилен JSON, 401, 429 и фрлен ConnectionException. Исто така, користете Http::assertSent() за да ги потврдите endpoint-от и клучевите на query-параметрите, додека намерно избегнувате тврдења што откриваат вистински токен.
Безбедност, набљудливост и распоредување
Никогаш не го логирајте query string-от: тој ги содржи и токенот и адресата на е-пошта на корисникот. Логовите за услугата погоре бележат само категорија на неуспех, HTTP статус, број на обид и класа на исклучок. Испраќајте ги предупредувачките и грешковните логови до вашата вообичаена дестинација за предупредувања, потоа следете ги одложените регистрации, неуспесите при автентикација, ограничувањата на стапката и соодносот на отфрлени со дозволени одлуки.
Распоредете ја миграцијата пред кодот што ја запишува новата колона. Конфигурирајте го токенот независно во секоја околина, потоа кеширајте ја конфигурацијата и извршете ги тестовите:
php artisan migrate --force
php artisan config:cache
php artisan test
При ротација на токенот, ажурирајте ја тајната во околината, повторно изградете го кешот на конфигурацијата на Laravel и потврдете барање пред повторно да генерирате. Запомнете дека повторното генерирање веднаш го поништува претходниот активен токен.
Вообичаени неуспеси
- Секое барање е одложено: проверете ги распоредениот токен, кешираната конфигурација, конфигурацијата за статус на успех, излезниот HTTPS пристап и логовите на апликацијата.
- Автентикацијата не успева по ротацијата: едно распоредување сè уште го има поништениот токен или застарен кеш на конфигурацијата.
- Се појавуваат одговори 429: проверете го користењето на планот и злоупотребата на регистрацијата. Не го умножувајте проблемот со непосредни повторни обиди.
- Валидните одговори се означени како неправилни: споредете го вистинскиот одговор со официјалната документација и приспособете ја границата само за документирани облици.
- Тестовите контактираат продукција: осигурете се дека секој релевантен тест инсталира
Http::fake(); никогаш не ставајте активен токен во тест-фигурации.
Конечна листа за проверка
- Планот за услугата е активен и токенот ограничен на услугата доаѓа од страницата со документација.
- Токенот постои само во складиште за тајни поддржано од околината.
- Клиентот го повикува точниот GET endpoint со
tokenиemail. - Истеци на време за поврзување и одговор се ограничени.
- Само привремените неуспеси на врската и gateway неуспесите добиваат еден одложен повторен обид.
- Сите пет договорени полиња во одговорот се валидираат на API границата.
- Изречно блокираните препораки ја запираат регистрацијата.
- Истеци на време, ограничувања на квотата, неправилни одговори и прекини на API ја одложуваат валидацијата без да го отфрлат корисникот.
- Логовите не содржат токени, адреси на е-пошта и сурови тела на одговорите.
- Функционалните тестови ги докажуваат и патеката на отфрлање и патеката на регистрација со отворен неуспех.
Трајната поука не е само да повикате API за валидација на е-пошта. Таа е да одлучите што смее да контролира зависноста. Сигурен сигнал за ризик може да го заштити формуларот за регистрација, но привремен инфраструктурен проблем не треба да стане пресуда за вистинска личност. Со тоа што одложи се прави првокласен доменски исход, интеграцијата останува корисна кога услугата е здрава и човечна кога не е.