Laravel регистрација: Задржете ги корисниците да се регистрираат кога валидацијата на е-поштата затајува
Формуларот за регистрација има една задача: да им овозможи на легитимните луѓе да создадат сметка. Валидацијата на е-поштата го подобрува тој тек со филтрирање на очигледни грешки и адреси со висок ризик, но станува контрапродуктивна кога привремен прекин на зависна услуга ги блокира сите.
Ова упатство гради Laravel интеграција ориентирана кон продукција што проверува синтакса, домен, MX записи, сигнали од давателот и практичен ризик за испорака преку API-то Email Validator. Дефинитивните препораки за одбивање ја запираат регистрацијата. Истекувања на време, исцрпена квота, ограничувања на стапката, невалидни одговори и привремени грешки на серверот создаваат структуриран резултат „недостапно“ и ѝ дозволуваат на регистрацијата да продолжи.
Дизајнот е намерно синхрон затоа што резултатот влијае на тековното поднесување на формуларот. Исто така е намерно отворен при неуспех за оперативни грешки. Стандардниот тек на Laravel за потврдување е-пошта треба да остане конечниот доказ дека подносителот ја контролира адресата.
Предуслови и пристап до услугата
Ви треба PHP 8.3 или понов, тековна Laravel апликација, Composer, конфигурирана база на податоци и стандардниот User модел и формулар за регистрација на Laravel. Примерите ги користат вградениот HTTP клиент, валидација, евидентирање, ограничување на стапката и тест-лажни одговори на Laravel; не е потребен дополнителен HTTP пакет.
Обезбедете пристап до услугата пред да ја напишете интеграцијата:
- Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Email Validator.
- Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за Email Validator.
- Најдете го панелот Service token и копирајте го токенот со опсег на услугата.
Оваа услуга бара токен. Неговото повторно генерирање го поништува претходно активниот токен, затоа координирајте ја ротацијата со распоредувањето: ажурирајте ја тајната во околината, повторно распоредете или вчитајте ја конфигурацијата, потврдете го новиот токен и дури потоа сметајте дека промената е завршена.
Потврдете го точниот барање
API повикот е GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Автентикацијата го користи параметарот за прашање token, додека адресата се испраќа во email. Направете едно минимално барање со тест-адреса што не е за продукција:
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'
Акредитив во низа за прашање може да се појави во историјата на школката и во дневниците за пристап на инфраструктурата. Користете ја командата само во контролирана околина, задржете го местодржачот во споделената документација и конфигурирајте ги прокси-серверите и дневниците на апликацијата да ги редигираат низите за прашања.
Чувајте го вистинскиот токен во конфигурацијата на околината на Laravel, никогаш во контрола на изворниот код:
# .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
Додајте ја соодветната конфигурација во config/services.php. Читањето на променливите на околината тука го одржува кодот на апликацијата компатибилен со 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'))
)),
],
];
Конфигурирачкиот речник е важен. Доставениот договор ги наведува status, score, recommendation, checks и quota, но кодот на апликацијата не треба да претпоставува недокументирани значења или прагови за оценки. Потврдете ги тековно документираните вредности за препораки и соодветно конфигурирајте ги листите.
Архитектура: авторитативно одбивање, постепена деградација
Контролерот за регистрација прво извршува евтина локална валидација. Потоа посебна API граница го преведува оддалечениот одговор во еден од три исходи во доменот:
- Дозволи: одговорот е целосен и неговата препорака се совпаѓа со конфигурираната листа за дозволување.
- Одбиј: одговорот е целосен и неговата препорака се совпаѓа со конфигурираната листа за одбивање.
- Недостапно: проблеми со транспортот, автентикацијата, квотата, шемата, статусот или непозната препорака спречуваат доверлива одлука.
Само Reject ја запира регистрацијата. Unavailable се евидентира и се остава отворено при неуспех. Така проблемите со инфраструктурата остануваат различни од доказите дека адресата треба да биде одбиена.
Мапирајте го API одговорот на една граница
Создадете 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);
}
}
Сега создадете app/Services/EmailValidation/EmailValidatorClient.php. Тој користи кратки, ограничени истекувања на време и повторува само неуспеси на поврзување и одговори 5xx од страната на серверот. Не повторува слепо неуспеси на автентикација, невалидни барања или HTTP 429 одговори.
<?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;
}
}
Сите пет компоненти на одговорот учествуваат во доверливоста: status мора да сигнализира успех, score мора да биде нумерички, recommendation го определува резултатот на политиката, checks мора да содржи структурирани докази, а quota се проверува за исцрпеност. Апликацијата избегнува да измисли праг за оценка бидејќи не треба да се претпоставува скала или праг без изричен договор за услугата.
Поврзете ја одлуката со регистрацијата
Создадете или ажурирајте 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');
}
}
Оставете го email_verified_at непоставен и барајте потврдување е-пошта на Laravel пред чувствителни дејства. API валидацијата го проценува квалитетот на адресата; таа не докажува сопственост.
Заштитете ја квотата од злоупотребни поднесувања
Регистрирајте именуван ограничувач во app/Providers/AppServiceProvider.php, а потоа прикачете го на рутата за регистрација:
<?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']);
Ограничувањето на стапката припаѓа пред повикот што се плаќа или е ограничен со квота. Оставете го CSRF посредникот на Laravel овозможен, валидирајте локално пред да ја повикате услугата и никогаш не вклучувајте ги токенот, целосниот URL на барањето, суровата е-пошта или целосниот оддалечен одговор во дневниците.
Тестирајте ги патеките на одбивање и неуспех детерминистички
Http::fake() спречува тестовите да трошат квота или да зависат од мрежата. Низите со препораки во фикстурите подолу ја претставуваат конфигурираната политика на апликацијата; сменете ги заедно ако документираниот речник на услугата се разликува.
<?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);
}
}
Додајте уште еден функционален тест што потврдува дека конфигурираното одбивање повторно го прикажува формуларот, го зачувува внесот што не е таен, не создава корисник и никогаш не ја враќа внатрешната причина на давателот до напаѓач.
Распоредување и набљудливост
Поставете го токенот преку складиштето за тајни на вашата платформа за распоредување, потврдете дека продукцијата го содржи планираниот речник за препораки и повторно изградете ги кешовите на Laravel:
php artisan test
php artisan config:clear
php artisan config:cache
php artisan route:cache
Не ставајте живо API барање во крајната точка за здравје на апликацијата; честите проверки може да трошат квота и да направат здравјето на распоредувањето да зависи од изборна надворешна услуга. Наместо тоа, користете контролирана пробна регистрација во средина за поставување.
Поставете предупредување за траен раст на registration.email_validation_bypassed, неуспеси на автентикација, ограничувања на стапката, исцрпување на квотата и непознати препораки. Краток неуспех на поврзувањето е вообичаена деградација. Трајна грешка при автентикација обично значи истечен или повторно генериран токен, додека непозната препорака често сигнализира промена на договорот или конфигурацијата.
Вообичаени начини на неуспех
- Секое барање е недостапно: проверете ја кешираната конфигурација, активацијата на токенот, правописот на крајната точка, излезниот HTTPS пристап и конфигурираниот статус на успех.
- Повторно генериран токен сè уште не успева: заменете ја тајната во секоја активна инстанца и повторно изградете ја или рестартирајте ја кешираната конфигурација.
- HTTP 429 одговорите се зголемуваат: проверете ги контролите за злоупотреба и планирајте ја квотата. Не го засилувајте проблемот со повторувања.
- Одговорите што изгледаат валидно се мапираат како недостапни: споредете ја вистинската документирана структура на одговорот и речникот за препораки со граничниот мапер. Нека непознатите вредности останат отворени при неуспех и набљудливи.
- Латентноста на регистрацијата расте: проверете го времето на поврзување и повторувањата на 5xx. Зачувајте ги строгите граници за истекување на време наместо да чекате неограничено.
Конечна контролна листа за проверка
- Точната GET крајна точка прима само параметри за прашање
tokenиemail. - Токенот на услугата постои само во конфигурација на тајни поддржана од околината.
- Локалната валидација и ограничувањето на стапката се извршуваат пред надворешното барање.
- Статусот, оценката, препораката, проверките и квотата се валидираат на API границата.
- Само конфигурираните, авторитативни препораки одбиваат адреса.
- Истекувања на време, 429 одговори, исцрпување на квота, невалидни товари и привремени 5xx неуспеси ѝ дозволуваат на регистрацијата да продолжи.
- Дневниците содржат структурирани оперативни податоци, но не и токен или сурова адреса на е-пошта.
- Потврдата за сопственост на е-пошта сè уште ги заштитува чувствителните функции на апликацијата.
- Автоматизираните тестови покриваат одбивање, исцрпување на повторувања и ограничување на стапката што не се повторува.
Најсигурната порта за регистрација не е онаа што се преправа дека зависностите никогаш не откажуваат. Таа е онаа што ја знае разликата меѓу лоша адреса и лошо попладне на мрежата. Зачувајте ја таа разлика, и валидацијата на е-пошта ќе ја зајакне влезната врата без да стане бравата што ги заробува легитимните корисници надвор.