Форми за контакт во Laravel: сигурна валидација на е-пошта со кеширање и резервна опција
Контакт-формата треба да отфрла очигледно невалидни адреси без да стане зависна од друга услуга за прифаќање легитимни пораки. Таа тензија е суштината на проверката на е-пошта подготвена за продукциска средина. Самата синтаксна валидација пропушта непостоечки домени и ризични сигнали за испорака, додека строгата далечинска зависност може да претвори прекин кај давателот во нефункционална контакт-страница.
Овој туторијал создава Laravel контакт-форма што повикува услуга за валидација на е-пошта, го претвора нејзиниот одговор во одлука на ниво на апликација, кешира успешни проценки и дозволува продолжување кога валидацијата е привремено недостапна. Формата и понатаму ја користи локалната валидација на Laravel како прва линија на одбрана и никогаш не го изложува токенот на услугата во прелистувачот.
Добијте пристап и копирајте го токенот на услугата
Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Email Validator.
- Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го токенот ограничен на услугата.
- Зачувајте го во конфигурацијата на околината на Laravel, никогаш во PHP или JavaScript што се предава во репозиториумот.
Оваа услуга бара токен. Автентикацијата го користи параметарот за пребарување token={serviceToken}. Повторното генерирање на токенот го поништува претходно активниот токен, па ротацијата на токени мора да вклучува ажурирање на секоја распоредена околина што го користи.
Потврдете ја крајната точка пред да пишувате Laravel код
Точниот API повик е GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Тој ги прифаќа параметрите за пребарување email и token.
curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode '[email protected]' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN'
Прегледајте го овој одговор заедно со официјалната документација пред да изберете деловни прагови. Интеграцијата подолу ги користи status, score, recommendation, checks и quota, но ги валидира нивните типови наместо да претпоставува дека секој успешен HTTP одговор има употребливо тело.
Додајте вредности специфични за распоредувањето во .env:
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_MIN_SCORE=60
EMAIL_VALIDATOR_DENY_RECOMMENDATIONS=reject,invalid,undeliverable
EMAIL_VALIDATOR_CACHE_HOURS=12
[email protected]
Прагот за оценка и листата за одбивање се политика на апликацијата, а не тврдење дека тоа се единствените можни вредности за препорака на услугата. Усогласете ги со тековната документација, вашиот тест-одговор и вашата толеранција за лажни одбивања.
Архитектура: строго на границата, отпорно кај формата
Барањето следи намерно кратка патека:
- Laravel локално ги валидира името, синтаксата на е-поштата и пораката.
- Наменски клиент ја нормализира е-поштата и проверува хеширан клуч во кешот.
- При промашување на кешот, серверот го повикува валидаторот со ограничени временски истекувања.
- DTO го валидира и мапира надворешниот одговор.
- Политиката враќа
allow,denyилиunavailable. denyвраќа грешка за поле;unavailableја прифаќа пораката според политиката за грациозно резервно однесување.
Дозволувањето продолжување е соодветно за обична контакт-форма бидејќи губењето вистинско барање обично е полошо од примањето една сомнителна адреса. Ресетирањата лозинка, проверките на сопственост на сметка и финансиските работни текови треба да користат поинаква политика.
Релевантните проектни датотеки се config/services.php, app/Services/EmailValidator.php, app/Data/EmailAssessment.php, app/Http/Requests/ContactRequest.php, app/Http/Controllers/ContactController.php, app/Mail/ContactMessage.php, Blade погледите, routes/web.php и функционален тест.
Конфигурирајте ја границата на услугата
Додајте го следниов запис во низата што ја враќа config/services.php:
'email_validator' => [
'url' => 'https://ai.mihajlo.mk/api/email-validator/v1/check-email',
'token' => env('EMAIL_VALIDATOR_TOKEN'),
'minimum_score' => (float) env('EMAIL_VALIDATOR_MIN_SCORE', 60),
'deny_recommendations' => array_values(array_filter(array_map(
'trim',
explode(',', env(
'EMAIL_VALIDATOR_DENY_RECOMMENDATIONS',
'reject,invalid,undeliverable'
))
))),
'cache_hours' => (int) env('EMAIL_VALIDATOR_CACHE_HOURS', 12),
],
Конфигурациската индирекција овозможува безбедно користење на кешот за конфигурација на Laravel и ги задржува тајните надвор од контролата на изворниот код. Не повикувајте env() од класите на апликацијата.
Мапирајте го одговорот во доменски резултат
Создадете app/Data/EmailAssessment.php. Маперот прифаќа полиња на највисоко ниво или полиња во објект data, а потоа ги отфрла неправилно форматираните одговори на границата на апликацијата.
<?php
namespace App\Data;
final readonly class EmailAssessment
{
public function __construct(
public string $decision,
public ?string $status = null,
public int|float|null $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('unavailable', failure: $failure);
}
public static function fromPayload(array $json): self
{
$data = is_array($json['data'] ?? null) ? $json['data'] : [];
$body = array_merge($json, $data);
$status = $body['status'] ?? null;
$score = $body['score'] ?? null;
$recommendation = $body['recommendation'] ?? null;
$checks = $body['checks'] ?? null;
$quota = $body['quota'] ?? null;
if (! is_scalar($status)
|| ! is_numeric($score)
|| ! is_string($recommendation)
|| $recommendation === ''
|| ! is_array($checks)
|| $checks === []
|| ! is_array($quota)) {
return self::unavailable('malformed_response');
}
$normalizedStatus = strtolower((string) $status);
$normalizedRecommendation = strtolower(trim($recommendation));
$deny = array_map(
fn (string $value) => strtolower($value),
config('services.email_validator.deny_recommendations', [])
);
$remaining = data_get($quota, 'remaining');
if (in_array($normalizedStatus, ['error', 'failed', 'failure'], true)) {
return self::unavailable('service_status');
}
if (is_numeric($remaining) && (float) $remaining <= 0) {
return self::unavailable('quota_exhausted');
}
$decision = (float) $score
< (float) config('services.email_validator.minimum_score')
|| in_array($normalizedRecommendation, $deny, true)
? 'deny'
: 'allow';
return new self(
decision: $decision,
status: (string) $status,
score: (float) $score,
recommendation: $recommendation,
checks: $checks,
quota: $quota,
);
}
}
Целосната структура checks останува достапна за идно усовршување на политиката без поврзување на формата со недокументирани вгнездени полиња. Неправилен резултат станува unavailable, никогаш случајно одбивање.
Повикајте го API со кеширање, временски истекувања и селективни повторни обиди
Создадете app/Services/EmailValidator.php:
<?php
namespace App\Services;
use App\Data\EmailAssessment;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
final class EmailValidator
{
public function check(string $email): EmailAssessment
{
$normalized = strtolower(trim($email));
$cacheKey = 'email-validator:'.hash('sha256', $normalized);
if ($cached = Cache::get($cacheKey)) {
return $cached;
}
$token = config('services.email_validator.token');
if (! is_string($token) || $token === '') {
Log::error('Email validator token is not configured');
return EmailAssessment::unavailable('missing_token');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = Http::acceptJson()
->connectTimeout(2)
->timeout(5)
->get(config('services.email_validator.url'), [
'email' => $normalized,
'token' => $token,
]);
} catch (ConnectionException) {
if ($attempt < 3) {
usleep($attempt === 1 ? 100_000 : 300_000);
continue;
}
Log::warning('Email validator connection failed', [
'email_hash' => substr(hash('sha256', $normalized), 0, 12),
]);
return EmailAssessment::unavailable('connection_failed');
}
if (in_array($response->status(), [500, 502, 503, 504], true)
&& $attempt < 3) {
usleep($attempt === 1 ? 100_000 : 300_000);
continue;
}
if ($response->status() === 429) {
Log::warning('Email validator rate or quota limit reached');
return EmailAssessment::unavailable('rate_limited');
}
if (! $response->successful()) {
Log::warning('Email validator rejected the request', [
'http_status' => $response->status(),
]);
return EmailAssessment::unavailable('http_'.$response->status());
}
$json = $response->json();
$assessment = is_array($json)
? EmailAssessment::fromPayload($json)
: EmailAssessment::unavailable('invalid_json');
if ($assessment->decision !== 'unavailable') {
Cache::put(
$cacheKey,
$assessment,
now()->addHours(
config('services.email_validator.cache_hours', 12)
)
);
}
Log::info('Email validation completed', [
'decision' => $assessment->decision,
'status' => $assessment->status,
'score' => $assessment->score,
'recommendation' => $assessment->recommendation,
]);
return $assessment;
}
return EmailAssessment::unavailable('unexpected_failure');
}
}
Се повторуваат само неуспеси на поврзувањето и веројатно привремени неуспеси на серверот. Грешките при автентикација, невалидните барања и одговорите за квота не се повторуваат слепо. Неуспешните проценки не се кешираат, што овозможува опоравување веднаш штом услугата повторно стане достапна.
Поврзете го валидаторот со контакт-формата
Создадете го барањето со php artisan make:request ContactRequest, па дефинирајте ги неговите правила:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class ContactRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email:rfc', 'max:254'],
'message' => ['required', 'string', 'min:10', 'max:5000'],
];
}
}
Создадете стандардна Laravel mailable-класа со име ContactMessage, чиј конструктор ги изложува $senderName, $senderEmail и $body, и чиј поглед за содржина е mail.contact. Во тој Blade поглед, испишувајте вредности со escape Blade изрази како {{ $senderEmail }}; не ја прикажувајте пораката со неескејпирана синтакса.
Контролерот врши далечинска валидација пред испраќањето:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Mail\ContactMessage;
use App\Services\EmailValidator;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
final class ContactController extends Controller
{
public function store(
ContactRequest $request,
EmailValidator $validator
): RedirectResponse {
$data = $request->validated();
$assessment = $validator->check($data['email']);
if ($assessment->decision === 'deny') {
return back()->withInput()->withErrors([
'email' => 'Please provide another deliverable email address.',
]);
}
if ($assessment->decision === 'unavailable') {
Log::notice('Contact accepted with validation fallback', [
'reason' => $assessment->failure,
]);
}
Mail::to(config('contact.to'))->send(new ContactMessage(
senderName: $data['name'],
senderEmail: $data['email'],
body: $data['message'],
));
return back()->with('status', 'Thanks. Your message has been sent.');
}
}
Создадете config/contact.php што враќа ['to' => env('CONTACT_TO')]. Регистрирајте ги рутите со заштита од злоупотреба:
use App\Http\Controllers\ContactController;
use Illuminate\Support\Facades\Route;
Route::view('/contact', 'contact')->name('contact');
Route::post('/contact', [ContactController::class, 'store'])
->middleware('throttle:10,1')
->name('contact.store');
Формата во resources/views/contact.blade.php треба да објавува кон route('contact.store'), да го вклучува Laravel-овиот @csrf, да прикажува грешки при валидација и да користи вредности од old(). CSRF заштитата, ограничувањето на барања, лимитите на должина и ескејпираниот излез ја покриваат основната површина за напад на контакт-формата.
Тестирајте ги одлуките, кеширањето и резервното однесување
Laravel HTTP fake ги прави тестовите детерминистички и осигурува дека ниту еден токен на услугата не го напушта тест-процесот.
<?php
namespace Tests\Feature;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
final class ContactTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Cache::flush();
config([
'services.email_validator.token' => 'test-token',
'services.email_validator.minimum_score' => 60,
'services.email_validator.deny_recommendations' => ['invalid'],
'contact.to' => '[email protected]',
]);
}
public function test_safe_email_is_checked_cached_and_sent(): void
{
Mail::fake();
Http::fake([
'https://ai.mihajlo.mk/api/email-validator/v1/check-email*' =>
Http::response([
'status' => 'success',
'score' => 92,
'recommendation' => 'safe',
'checks' => ['assessment_available' => true],
'quota' => ['remaining' => 20],
]),
]);
$payload = [
'name' => 'Ada',
'email' => '[email protected]',
'message' => 'Please send the project details.',
];
$this->post('/contact', $payload)->assertSessionHas('status');
$this->post('/contact', $payload)->assertSessionHas('status');
Http::assertSentCount(1);
Http::assertSent(fn ($request) =>
$request['email'] === '[email protected]'
&& $request['token'] === 'test-token'
);
Mail::assertSent(ContactMessage::class, 2);
}
public function test_low_score_is_rejected(): void
{
Mail::fake();
Http::fake([
'*' => Http::response([
'status' => 'success',
'score' => 25,
'recommendation' => 'invalid',
'checks' => ['assessment_available' => true],
'quota' => ['remaining' => 19],
]),
]);
$this->post('/contact', [
'name' => 'Ada',
'email' => '[email protected]',
'message' => 'This message is long enough.',
])->assertSessionHasErrors('email');
Mail::assertNothingSent();
}
public function test_service_failure_falls_back_to_accepting_message(): void
{
Mail::fake();
Http::fake(['*' => Http::response([], 503)]);
$this->post('/contact', [
'name' => 'Ada',
'email' => '[email protected]',
'message' => 'Please send the project details.',
])->assertSessionHas('status');
Mail::assertSent(ContactMessage::class);
}
}
Извршете го пакетот тестови со php artisan test. Примерите намерно содржат само лажни ингеренции и синтетички податоци за одговори.
Распоредување, набљудливост и чести неуспеси
Користете споделен кеш како Redis кога апликацијата работи на повеќе инстанци; во спротивно секој јазол ќе прави сопствени повици за валидација. По поставувањето на вредностите за продукциската околина, извршете php artisan config:cache. Осигурете се дека конфигурираниот транспорт за пошта работи пред да ја овозможите јавната рута.
Следете ги одлуките за валидација, бројот на резервни продолжувања, HTTP статусните кодови, латентноста на инфраструктурно ниво и преостанатата квота кога таа вредност е присутна. Никогаш не ги евидентирајте токенот, целосниот URL на барањето, необработената е-пошта, телото на пораката или целосниот API одговор. Бидејќи автентикацијата се пренесува во низа за пребарување, прегледајте ги евиденциите на прокси и HTTP-клиентот за да се осигурате дека параметрите за пребарување се редигирани.
Честите одговори 401 или 403 обично укажуваат на токен што недостасува, е поништен или е неправилно распореден. 429 треба да поттикне истрага на квотата и сообраќајот, а не агресивни повторни обиди. Повторените резервни продолжувања поради неправилен одговор укажуваат на отстапување во договорот или неочекувана обвивка за грешка. Ако секое барање го промашува кешот, потврдете го двигателот на продукцискиот кеш, дозволите, TTL и дали распоредувањата го споделуваат истиот кеш.
Контролна листа за финална проверка
- Планот на услугата е активен и тековниот токен ограничен на услугата е присутен само во конфигурацијата на околината.
- GET барањето ги испраќа двата параметри за пребарување
emailиtokenдо точната крајна точка. - Локалната валидација се извршува пред далечинското барање.
- Статусот, оценката, препораката, проверките и квотата се мапирани дефанзивно.
- Само завршените проценки се кешираат под хеширани клучеви за е-пошта.
- Неуспесите при автентикација, валидација и квота не се повторуваат слепо.
- Недостапноста на валидаторот ја дозволува контакт-пораката, додека емитува структурирана евиденција за резервно продолжување.
- Воспоставени се CSRF заштита, ограничување на барања, безбедно ескејпирање на излезот, конфигурирана испорака на пошта и автоматизирани тестови.
Стабилна контакт-форма не е онаа со најагресивниот филтер. Таа е онаа што носи внимателна одлука кога има достапни докази, го зачувува патот на корисникот кога нема и остава доволно оперативни докази за да се разликуваат овие случаи.