Diese Anleitung gilt für Laravel 11 und neuer, mit seinem HTTP-Client. Zwei Einstellungen sind wichtiger als der Aufruf: Laravel verschlüsselt jedes Cookie, das es liest, sofern nicht anders angegeben, und es ignoriert Proxys, bis Sie ihnen vertrauen.
Der Tag
Im Layout, das Ihre Registrierungs-, Anmelde- und Checkout-Seiten verwenden:
{{-- resources/views/layouts/app.blade.php, in <head> --}}
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
Der Aufruf
In bootstrap/app.php lassen Sie das Cookie des Tags unverschlüsselt und vertrauen Ihrem Proxy:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->encryptCookies(except: ['_kz']); // the tag's cookie is not Laravel's
$middleware->trustProxies(at: '*'); // only if the app is reachable through your proxy alone
})
Lesen Sie den Schlüssel über die Config, damit es auch nach php artisan config:cache funktioniert:
// config/services.php
'korzen' => [
'key' => env('KORZEN_API_KEY'),
],
Der Aufruf. timeout und connectTimeout nehmen Sekunden, auch Bruchteile:
// app/Services/Korzen.php
namespace App\Services;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Throwable;
class Korzen
{
/** The headers Bot Protection reads: those one browser build sends alike, and Accept-Language. */
private const HEADERS = ['accept', 'accept-encoding', 'accept-language', 'sec-ch-ua', 'sec-ch-ua-mobile',
'sec-ch-ua-platform', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site', 'upgrade-insecure-requests'];
/** The answer for this request, or null on any error: then go on as on allow (fail open). */
public static function decide(Request $request, string $path): ?array
{
$headers = [];
foreach (self::HEADERS as $name) {
if ($request->hasHeader($name)) {
$headers[$name] = $request->header($name);
}
}
// The order the headers came in, as $_SERVER keeps it: PHP-FPM lists them last first.
$names = array_values(array_filter(array_keys($request->server->all()), fn ($k) => str_starts_with((string) $k, 'HTTP_')));
if (PHP_SAPI === 'fpm-fcgi') {
$names = array_reverse($names);
}
$order = array_map(fn ($k) => strtolower(strtr(substr($k, 5), '_', '-')), array_slice($names, 0, 64));
try {
$res = Http::withToken(config('services.korzen.key'))
->timeout(0.2)
->connectTimeout(0.2)
->post('https://korzen.dev/v1/decide', [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'headers' => (object) $headers, // {} when empty, never []
'header_order' => $order,
'token' => $request->cookie('_kz'),
'path' => $path,
'method' => 'POST',
]);
return $res->ok() ? $res->json() : null;
} catch (Throwable) {
return null;
}
}
}
header_order ist die Reihenfolge, in der die Header der Anfrage ankamen: Ein Skript, das die Werte eines Browsers kopiert, kopiert selten ihre Reihenfolge. PHP-FPM hält sie in den HTTP_*-Schlüsseln von $_SERVER, von hinten nach vorn, also kehrt der Aufruf sie dort um; php artisan serve hält sie in Reihenfolge. Hinter nginx ist es die Reihenfolge, in der nginx sie weitergibt, also die, in der sie ankamen (gemessen mit nginx 1.31); nginx verwirft einen Namen mit _. Hinter einem Load Balancer oder Cloudflare ist es die Reihenfolge dieses Hops, nicht die des Browsers.
Dann dort, wo das Konto angelegt wird; mit Breeze in RegisteredUserController::store:
// app/Http/Controllers/Auth/RegisteredUserController.php (Breeze), in store()
$decision = Korzen::decide($request, '/register');
if (($decision['action'] ?? 'allow') === 'block') {
throw ValidationException::withMessages(['email' => 'We could not create your account.']);
}
// "challenge": your CAPTCHA, or an email or phone check, before going on
$user = User::create([
// ... your fields
'korzen_request_id' => $decision['request_id'] ?? null,
]);
Die IP des Besuchers
$request->ip() ist die Adresse Ihres Proxys, bis Sie ihm vertrauen. trustProxies(at: '*') vertraut allem, was sich verbindet, was richtig ist, wenn die App nur über Ihren Load Balancer oder Cloudflare erreichbar ist; sonst tragen Sie die Adressen Ihres Proxys ein. PHP-FPM öffnet für jede bediente Anfrage eine neue Verbindung, sodass jeder Aufruf einen TLS-Handshake mitbringt: Weit weg von Europa wählen Sie einen Timeout mit Spielraum dafür.
Die Aktion anwenden
Lesen Sie nur action. Bei block antworten Sie mit demselben Fehler, den die Aktion zeigt, wenn sie aus einem anderen Grund scheitert, damit ein Bot nichts lernt. Bei challenge nutzen Sie die Prüfung, die Sie schon haben (ein CAPTCHA, ein Code per E-Mail oder Telefon); ohne eine machen Sie weiter. Das Konto beginnt im Monitor-Modus, in dem action immer allow ist: Für Besucher ändert sich nichts, bis Sie den Block-Modus einschalten.
Die request_id aufbewahren
Jede Antwort hat eine request_id. Bewahren Sie sie bei dem auf, was die Aktion erstellt hat, in einer Spalte des Kontos oder der Bestellung, oder in einer Logzeile mit deren ID. Sie benennt diese Entscheidung, wenn Sie uns dazu etwas fragen, und lässt Sie später herausfinden, welche Antwort ein Fake-Konto durchgelassen hat.
Auf Ihrem Rechner testen
Führen Sie php artisan serve aus und öffnen Sie http://localhost:8000. Seiten auf localhost akzeptieren den Tag immer und erscheinen nie als Domain. Lokal ist die IP 127.0.0.1 oder ::1, und das ist in Ordnung. Läuft die App auf Ihrem Rechner in Docker, sieht sie stattdessen die Adresse der Bridge, etwa 172.17.0.1: Die Challenge-Seite bringt Sie trotzdem zu localhost zurück, denn der Tag hat Ihre Seite dort gesehen.
- Setzen Sie
KORZEN_API_KEYin.envund öffnen Sie die Registrierungsseite. In den Entwicklertools des Browsers erscheint nach ein paar Sekunden das_kz-Cookie. - Registrieren Sie sich. Die Entscheidung erscheint auf der Seite Entscheidungen des Dashboards, unter dem Pfad, den Sie gesendet haben.
- Prüfen Sie fail-open: Setzen Sie einen falschen Schlüssel und registrieren Sie sich erneut. Der Aufruf antwortet
401, und die Registrierung geht durch.
Häufige Fehler
_kzfehlt inencryptCookies(except: …). Laravel kann das Cookie des Tags dann nicht entschlüsseln, und$request->cookie('_kz')ist bei jeder Anfragenull.env('KORZEN_API_KEY')im Service. Nachconfig:cachegibtenv()außerhalb der Config-Dateiennullzurück.- Keine vertrauenswürdigen Proxys hinter einem Load Balancer. Jeder Besucher hat dann die IP des Balancers.
/v1/decideaus dem Browser aufrufen. Der API key würde jeden Besucher erreichen; der Aufruf gehört auf Ihren Server.- Kein Timeout, oder ein Fehler, der die Registrierung stoppt. Jede andere Antwort als
200, und keine Antwort rechtzeitig, bedeutet: den Besucher durchlassen.