Laravel клиентски контролни табли: Интегрирајте анализатор за безбедност на веб-страници за практични увидувања за клиентите
Безбедносниот извештај станува корисен само кога некој може да види што се променило, да разбере што е важно и да ги претвори препораките во завршена работа. За мала веб-агенција, тоа значи повеќе од сместување API-одговор во убава картичка. Контролната табла има потреба од трајна историја, извршување во заднина, експлицитни состојби на неуспех и задачи за санација што го преживуваат следното скенирање.
Овој туторијал го гради тој работен тек во Laravel и PHP 8.3+. Секој клиент има HTTPS веб-страница, хронолошка историја на скенирања, наоди групирани според сериозност, TLS-детали и применливи препораки. Website Security Analyzer извршува ограничена, неинвазивна анализа на јавната HTTPS и безбедносната положба на прелистувачот. Треба да поддржува рутинска хигиена и приоритизација, но никогаш не смее да се опишува како пенетрационо тестирање.
Добијте пристап пред да пишувате код за интеграција
- Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Website Security Analyzer. Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата. Најдете го панелот Service token и копирајте го токенот со опсег на услугата.
- Зачувајте го тој токен во околината на апликацијата. Неговото повторно генерирање го поништува претходно активниот токен, затоа координирајте ја ротацијата со распоредувањето наместо да го генерирате повторно неформално.
API-то прифаќа Bearer токен, заглавие X-API-Token или параметар за пребарување token. Овој проект користи Bearer токен бидејќи ја задржува автентикацијата надвор од URL-адресите, дневниците за пристап и историјата на прелистувачот.
Точното барање е POST https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website, со JSON-тело што содржи url. Проверете го пристапот со минимално барање:
curl --fail-with-body \
--request POST \
--url https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website \
--header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
--header "Content-Type: application/json" \
--data '{"url":"https://client.example"}'
Сега ставете го акредитивот во .env, никогаш во PHP-изворот, фикстурите, сликите од екранот или дневниците:
WEBSITE_SECURITY_ANALYZER_TOKEN=YOUR_SERVICE_TOKEN
QUEUE_CONNECTION=database
Додајте конфигурација поддржана од околината во config/services.php:
'website_security_analyzer' => [
'endpoint' => 'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website',
'token' => env('WEBSITE_SECURITY_ANALYZER_TOKEN'),
],
Изберете архитектура што ја зачувува историјата
Барањето не треба да чека да заврши надворешната анализа. Контролер создава скенирање во исчекување, а потоа испраќа задача во редицата. Задачата повикува наменски API-клиент, го проверува одговорот на границата на апликацијата, зачувува непроменлива снимка и создава отворени задачи за санација од препораките.
Овој дизајн чини неколку табели во базата и работник за редица, но овозможува побрзи одговори во прелистувачот, контролирани повторни обиди и ревизорска трага. Исто така, ги задржува транспортните грижи надвор од контролерите и приказите.
Предусловите се PHP 8.3+, Composer, поддржана Laravel база на податоци и позадина за редица. Почнете со нормална Laravel апликација и генерирајте ги главните класи:
composer create-project laravel/laravel agency-security-dashboard
cd agency-security-dashboard
php artisan make:model Client -m
php artisan make:model SecurityScan -m
php artisan make:model RemediationTask -m
php artisan make:controller ClientSecurityController
php artisan make:job AnalyzeClientWebsite
php artisan make:policy ClientPolicy --model=Client
php artisan make:test WebsiteSecurityAnalyzerTest --unit
Релевантната структура е намерно мала:
app/
Data/WebsiteAnalysis.php
Exceptions/AnalyzerFailure.php
Http/Controllers/ClientSecurityController.php
Jobs/AnalyzeClientWebsite.php
Models/Client.php
Models/SecurityScan.php
Models/RemediationTask.php
Services/WebsiteSecurityAnalyzer.php
resources/views/clients/security.blade.php
routes/web.php
tests/Unit/WebsiteSecurityAnalyzerTest.php
Зачувајте ги снимките и задачите одделно
Скенирањето е доказ снимен во одреден момент. Задачата за санација е променлива работа. Нивното раздвојување ѝ овозможува на агенцијата да затвори задача без да го препишува историскиот одговор што ја создал.
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('website_url', 2048);
$table->timestamps();
});
Schema::create('security_scans', function (Blueprint $table) {
$table->id();
$table->foreignId('client_id')->constrained()->cascadeOnDelete();
$table->string('status')->index(); // pending, running, completed, failed
$table->double('score')->nullable();
$table->json('findings')->nullable();
$table->json('tls_details')->nullable();
$table->json('recommendations')->nullable();
$table->string('error_code')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Schema::create('remediation_tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('client_id')->constrained()->cascadeOnDelete();
$table->foreignId('security_scan_id')->constrained()->cascadeOnDelete();
$table->text('description');
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Конфигурирајте ги моделите со соодветните релации hasMany и belongsTo. Претворете ги JSON-колоните во array, а временските ознаки во datetime. Или декларирајте ги доделените полиња во $fillable или користете експлицитно доделување својства насекаде.
Потврдете го API-одговорот на една граница
Обезбедениот договор изложува резултат, наоди групирани според сериозност, TLS-детали и препораки. Адаптерот не треба да дозволи неочекувана HTML-страница за грешка или променет JSON-облик да навлезе во доменот.
<?php
namespace App\Data;
use App\Exceptions\AnalyzerFailure;
final readonly class WebsiteAnalysis
{
public function __construct(
public int|float $score,
public array $findings,
public array $tlsDetails,
public array $recommendations,
) {}
public static function fromPayload(mixed $payload): self
{
if (! is_array($payload)
|| ! is_int($payload['score'] ?? null) && ! is_float($payload['score'] ?? null)
|| ! is_array($payload['findings'] ?? null)
|| ! is_array($payload['tls'] ?? null)
|| ! is_array($payload['recommendations'] ?? null)
) {
throw new AnalyzerFailure('schema', 'Unexpected analyzer response.');
}
foreach ($payload['findings'] as $group => $items) {
if (! is_string($group) || ! is_array($items)) {
throw new AnalyzerFailure('schema', 'Invalid findings grouping.');
}
}
$recommendations = array_values(array_filter(
$payload['recommendations'],
static fn (mixed $value): bool => is_string($value) && trim($value) !== ''
));
return new self(
$payload['score'],
$payload['findings'],
$payload['tls'],
$recommendations,
);
}
}
Ако официјалната документација ги враќа овие полиња во документиран плик, отпакувајте го тој плик овде и никаде на друго место. Не расфрлајте претпоставени резервни решенија низ задачите и приказите.
Користете ограничени временски ограничувања и класифицирани неуспеси
<?php
namespace App\Services;
use App\Data\WebsiteAnalysis;
use App\Exceptions\AnalyzerFailure;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
final class WebsiteSecurityAnalyzer
{
public function analyze(string $url): WebsiteAnalysis
{
$token = config('services.website_security_analyzer.token');
if (! is_string($token) || $token === '') {
throw new AnalyzerFailure('configuration', 'Analyzer token is missing.');
}
try {
$response = Http::acceptJson()
->asJson()
->withToken($token)
->connectTimeout(5)
->timeout(30)
->post(
config('services.website_security_analyzer.endpoint'),
['url' => $url],
);
} catch (ConnectionException $exception) {
throw new AnalyzerFailure('temporary', 'Analyzer connection failed.', previous: $exception);
}
if ($response->successful()) {
return WebsiteAnalysis::fromPayload($response->json());
}
$status = $response->status();
if (in_array($status, [401, 403], true)) {
throw new AnalyzerFailure('authentication', 'Analyzer authentication failed.');
}
if ($status === 429) {
$header = $response->header('Retry-After');
$delay = ctype_digit((string) $header)
? max(30, min(900, (int) $header))
: 120;
throw new AnalyzerFailure('rate_limit', 'Analyzer rate limit reached.', $delay);
}
if ($status === 422) {
throw new AnalyzerFailure('validation', 'Analyzer rejected the website URL.');
}
if ($status >= 500) {
throw new AnalyzerFailure('temporary', 'Analyzer is temporarily unavailable.');
}
throw new AnalyzerFailure('upstream', "Unexpected analyzer status: {$status}.");
}
}
AnalyzerFailure е мала подкласа на RuntimeException што изложува јавни readonly-својства kind и nullable retryAfter. Нејзиниот конструктор треба да го прифати PHP-овиот именуван аргумент previous како што е прикажано.
Извршувајте скенирања во редицата
Само неуспесите на поврзувањето, неуспесите на серверот и ограничувањата на стапката заслужуваат уште еден обид. Неуспесите на валидацијата и автентикацијата бараат човечка интервенција, па нивното повторување само троши квота и го крие вистинскиот проблем.
final class AnalyzeClientWebsite implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 4;
public function __construct(public SecurityScan $scan) {}
public function backoff(): array
{
return [30, 120, 300];
}
public function handle(WebsiteSecurityAnalyzer $analyzer): void
{
$this->scan->update(['status' => 'running']);
try {
$result = $analyzer->analyze($this->scan->client->website_url);
} catch (AnalyzerFailure $failure) {
Log::warning('Website analysis failed', [
'scan_id' => $this->scan->id,
'client_id' => $this->scan->client_id,
'kind' => $failure->kind,
'attempt' => $this->attempts(),
]);
if ($failure->kind === 'rate_limit' && $this->attempts() < $this->tries) {
$this->release($failure->retryAfter ?? 120);
return;
}
if ($failure->kind === 'temporary') {
throw $failure;
}
$this->scan->update([
'status' => 'failed',
'error_code' => $failure->kind,
'completed_at' => now(),
]);
return;
}
DB::transaction(function () use ($result): void {
$this->scan->update([
'status' => 'completed',
'score' => $result->score,
'findings' => $result->findings,
'tls_details' => $result->tlsDetails,
'recommendations' => $result->recommendations,
'completed_at' => now(),
]);
foreach ($result->recommendations as $recommendation) {
$this->scan->remediationTasks()->create([
'client_id' => $this->scan->client_id,
'description' => $recommendation,
]);
}
});
}
public function failed(?Throwable $failure): void
{
$this->scan->update([
'status' => 'failed',
'error_code' => 'retry_exhausted',
'completed_at' => now(),
]);
}
}
Дејството на контролерот треба да овласти пристап, да го заклучи редот на клиентот во трансакција, да отфрли второ скенирање во исчекување или извршување, да го создаде записот во исчекување и да ја испрати задачата по потврдувањето:
public function store(Client $client): RedirectResponse
{
Gate::authorize('update', $client);
DB::transaction(function () use ($client): void {
$locked = Client::query()->lockForUpdate()->findOrFail($client->id);
abort_if(
$locked->securityScans()
->whereIn('status', ['pending', 'running'])
->exists(),
409,
'A scan is already in progress.'
);
$scan = $locked->securityScans()->create(['status' => 'pending']);
AnalyzeClientWebsite::dispatch($scan)->afterCommit();
});
return back()->with('status', 'Security analysis queued.');
}
Дефинирајте рути за контролната табла, создавање скенирање и завршување задачи. Политиката мора да осигури дека автентицираниот корисник е сопственик на клиентот; врзувањето на моделот со рутата само по себе не е авторизација.
Route::middleware('auth')->group(function (): void {
Route::get('/clients/{client}/security', [ClientSecurityController::class, 'show'])
->name('clients.security.show');
Route::post('/clients/{client}/security/scans', [ClientSecurityController::class, 'store'])
->name('clients.security.scans.store');
Route::patch('/clients/{client}/tasks/{task}', [ClientSecurityController::class, 'complete'])
->scopeBindings()
->name('clients.security.tasks.complete');
});
Blade-приказот може да остане едноставен: прикажете го најновиот резултат и времето на завршување, повторете низ findings по група на сериозност, прикажете ги TLS-деталите како означени вредности, наведете ги отворените задачи за санација со форми за завршување заштитени со CSRF и поставете ги претходните скенирања под тековната снимка. Екранирајте ги обичните вредности со Blade-овото {{ }}; не прикажувајте API-текст преку {!! !!}.
Тестирајте ја границата без да повикувате продукција
Laravel-овиот HTTP fake ги прави транспортните тестови детерминистички и докажува дека тајните и телата на барањата се составени правилно.
public function test_it_maps_a_successful_analysis(): void
{
config()->set('services.website_security_analyzer.token', 'test-token');
config()->set(
'services.website_security_analyzer.endpoint',
'https://ai.mihajlo.mk/api/website-security-analyzer-api/v1/analyze-website'
);
Http::fake([
'https://ai.mihajlo.mk/*' => Http::response([
'score' => 82,
'findings' => ['high' => [], 'medium' => []],
'tls' => [],
'recommendations' => ['Review the site security configuration.'],
], 200),
]);
$result = app(WebsiteSecurityAnalyzer::class)
->analyze('https://client.example');
$this->assertSame(82, $result->score);
$this->assertCount(1, $result->recommendations);
Http::assertSent(fn ($request) =>
$request->method() === 'POST'
&& $request['url'] === 'https://client.example'
&& $request->hasHeader('Authorization', 'Bearer test-token')
);
}
public function test_authentication_failure_is_not_retried(): void
{
config()->set('services.website_security_analyzer.token', 'invalid');
Http::fake(['https://ai.mihajlo.mk/*' => Http::response([], 401)]);
try {
app(WebsiteSecurityAnalyzer::class)->analyze('https://client.example');
$this->fail('Expected an authentication failure.');
} catch (AnalyzerFailure $failure) {
$this->assertSame('authentication', $failure->kind);
}
Http::assertSentCount(1);
}
Додајте тестови за задачи со Queue::fake() за испраќање и Http::fake() за завршување, ограничување на стапката, невалиден JSON и исцрпени привремени неуспеси. Потврдете ја состојбата на базата на податоци, а не внатрешните повици на методи.
Продукциски заштитни мерки и распоредување
- Прифаќајте само нормализирани јавни
https://URL-адреси на клиенти. Отфрлете акредитиви во URL-адреси, имиња localhost и приватни или резервирани IP-одредишта. - Шифрирајте ги тајните на околината преку платформата за распоредување и ограничете кој може да ги прегледува. За време на ротацијата, распоредете го повторно генерираниот токен насекаде каде што се користи редицата пред старите работници да продолжат со обработката.
- Никогаш не го запишувајте во дневник токенот, заглавието за авторизација, целосниот одговор или URL-адресата на клиентот. Стабилните ID-а на скенирањата, ID-а на клиентите, категории на неуспех, обиди, траење и категории на HTTP-статус обезбедуваат корисна набљудливост.
- Поставете предупредувања за повторени неуспеси на автентикација, неуспеси на шемата, исцрпени повторни обиди и растечка редица. Неуспех на шемата е аларм за интеграцијата, а не празен успешен извештај.
- Јасно означете ја контролната табла како ограничена, неинвазивна анализа на безбедноста на веб-страница. Избегнувајте јазик што подразбира потврдување на експлоатирање, покриеност на внатрешна мрежа или пенетрационо тестирање.
Распоредете ги миграциите на базата на податоци, повторно изградете ја конфигурацијата и рестартирајте ги работниците за да го вчитаат новиот токен и код:
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
php artisan queue:work --tries=4 --timeout=90
Во продукција, извршувајте го работникот под надзорник на процеси. Неговото временско ограничување на процесот мора да остане удобно подолго од временското ограничување на HTTP-одговорот. Вообичаените неуспеси обично се директни: 401 или 403 укажува на проблем со конфигурацијата или ротацијата на токенот; 422 укажува на неприфатлива URL-адреса; 429 бара одложен повторен обид или преглед на планот; повторените 5xx или неуспеси на поврзувањето укажуваат на привремен проблем нагоре по текот; а неуспех на шемата значи дека адаптерот мора да се спореди со тековната официјална документација.
Конечна контролна листа за проверка
- Клиентот може да стави едно скенирање во редица без да го држи отворено барањето на прелистувачот.
- Истовремените кликови не можат да создадат повеќе активни скенирања за истиот клиент.
- Успешните резултати ги задржуваат резултатот, групираните наоди, TLS-деталите, препораките и времето на завршување.
- Препораките стануваат задачи што можат да се затворат, додека оригиналното скенирање останува непроменливо.
- Авторизацијата спречува еден корисник на агенцијата да ги прегледува или ажурира клиентите на друг корисник.
- Неуспесите на автентикацијата и валидацијата запираат веднаш; преодните неуспеси користат ограничен backoff.
- Тестовите не прават мрежни повици и не содржат вистински токен за услугата.
- Контролната табла точно го опишува резултатот и никогаш не го нарекува пенетрационо тестирање.
Трајната вредност не е копчето за скенирање. Таа е циклусот околу него: сними ограничена проценка, зачувај го она што е забележано, претвори ги препораките во работа со јасен сопственик и врати се подоцна за да измериш што се променило. Тоа претвора безбедносен API од еднократен извештај во смирена, повторлива услуга за клиенти.