
Du brauchst kein Paket: Eine Klasse, die ValidationRule implementiert und über die Fassade Http die Adresse https://www.google.com/recaptcha/api/siteverify abfragt, genügt. Prüfe drei Dinge in der Antwort: success, die erwartete action und den score. Und kombiniere die Regel immer mit required: Laravel führt eine eigene Regel auf einem leeren Feld nicht aus.
Ein reCAPTCHA besteht aus zwei Dingen: einem Token, das der Browser erzeugt, und der Prüfung dieses Tokens bei Google, von deinem Server aus. Für den zweiten Teil bringt Laravel alles Nötige mit, und genau deshalb ist ein Paket von außen optional.
Artikel aus dem Lernpfad Webentwicklung. Dieser Leitfaden baut eine eigenständige Validierungsregel ohne Abhängigkeit und stellt sie in sechs Szenarien auf die Probe. Geprüft mit Laravel 13.30.1 und PHP 8.4.25.
Die Schlüssel besorgen
Lege in der reCAPTCHA-Konsole eine Site an und wähle die Version v3, die einen Score von 0 bis 1 vergibt, ohne dem Besucher etwas abzuverlangen. Du bekommst zwei Schlüssel: den öffentlichen Site-Key und den geheimen Schlüssel, der den Server nie verlassen darf.
RECAPTCHA_KEY=votre_cle_de_site
RECAPTCHA_SECRET=votre_cle_secreteTrag sie in die Konfiguration ein, statt env() aus dem Code heraus aufzurufen: Sobald die Konfiguration im Cache liegt, gibt env() überall außerhalb von config/ nur noch null zurück.
'recaptcha' => [
'key' => env('RECAPTCHA_KEY'),
'secret' => env('RECAPTCHA_SECRET'),
],Die Validierungsregel
Eine Klasse, die ValidationRule implementiert, genügt. Sie fragt die Prüf-API ab und kontrolliert den Erfolg, die erwartete Action und den Score.
php artisan make:rule Recaptcha<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
class Recaptcha implements ValidationRule
{
public function __construct(
private string $action,
private float $minScore = 0.5,
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || $value === '') {
$fail('La vérification anti-robot est absente.');
return;
}
$response = Http::asForm()
->timeout(5)
->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret'),
'response' => $value,
'remoteip' => request()->ip(),
]);
if ($response->failed()) {
$fail('La vérification anti-robot est indisponible. Réessayez.');
return;
}
$data = $response->json();
if (! ($data['success'] ?? false)) {
$fail('La vérification anti-robot a échoué.');
return;
}
if (($data['action'] ?? null) !== $this->action) {
$fail('La vérification anti-robot ne correspond pas à ce formulaire.');
return;
}
if (($data['score'] ?? 0) < $this->minScore) {
$fail('Votre requête a été considérée comme automatisée.');
}
}
}Die Prüfung der Action verdient eine Erklärung: Ohne sie könnte ein Token, das auf irgendeiner öffentlichen Seite deiner Site erzeugt wurde, die Validierung des Kontaktformulars bestehen. Der Score allein reicht nicht.
Die Regel ans Formular hängen
Laravel führt eine eigene Regel auf einem leeren oder fehlenden Wert nicht aus. Geprüft: Mit der Regel Recaptcha allein besteht ein leeres Feld die Validierung, ohne dass eine einzige Anfrage an Google geht. Es würde also reichen, das Feld nicht mitzuschicken, um das Captcha zu umgehen. Die Regel required ist hier nicht optional.
use App\Rules\Recaptcha;
public function store(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'message' => ['required', 'string', 'max:2000'],
'g-recaptcha-response' => ['required', new Recaptcha('contact', 0.5)],
]);
// …
}Das Formular im Browser
Das Formular unten stützt sich auf die Escaping-Regeln von Blade. Die v3 zeigt keine Checkbox: Das Skript erzeugt ein Token, das kurz vor dem Absenden in ein verstecktes Feld geschrieben wird.
<form method="POST" action="/contact" id="contact">
@csrf
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<input type="hidden" name="g-recaptcha-response" id="recaptcha-token">
<button type="submit">Envoyer</button>
</form>
@error('g-recaptcha-response')
<p class="erreur">{{ $message }}</p>
@enderror
<script src="https://www.google.com/recaptcha/api.js?render={{ config('services.recaptcha.key') }}"></script>
<script>
document.getElementById('contact').addEventListener('submit', function (e) {
e.preventDefault();
grecaptcha.ready(() => {
grecaptcha.execute(@json(config('services.recaptcha.key')), { action: 'contact' })
.then(token => {
document.getElementById('recaptcha-token').value = token;
e.target.submit();
});
});
});
</script>Die hier angegebene Action, contact, muss exakt die sein, die die Regel auf dem Server erwartet.
Testen, ohne Google aufzurufen
Ein Schutz, den niemand testet, ist ein Schutz, dessen Zustand niemand kennt. Denk außerdem daran, den Debug-Modus in der Produktion abzuschalten, sonst landet dein geheimer Schlüssel auf einer Fehlerseite.
Mit Http::fake() simulierst du jede Antwort der API:
use App\Rules\Recaptcha;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Facades\{Http, Validator};
$cas = [
'succès, score 0.9' => [200, ['success' => true, 'action' => 'contact', 'score' => 0.9]],
'succès, score 0.1' => [200, ['success' => true, 'action' => 'contact', 'score' => 0.1]],
'action différente' => [200, ['success' => true, 'action' => 'login', 'score' => 0.9]],
'jeton refusé par Google' => [200, ['success' => false, 'error-codes' => ['invalid-input-response']]],
'service indisponible' => [500, ''],
];
foreach ($cas as $label => [$code, $corps]) {
Http::swap(new Factory()); // startet für jeden Fall mit einem frischen Client
Http::fake(['*' => Http::response($corps, $code)]);
$v = Validator::make(
['g-recaptcha-response' => 'un-jeton'],
['g-recaptcha-response' => ['required', new Recaptcha('contact', 0.5)]],
);
printf("%-26s %s\n", $label, $v->passes() ? 'ACCEPTÉ' : 'refusé : '.$v->errors()->first());
}Die Ergebnisse:
succès, score 0.9 ACCEPTÉ
succès, score 0.1 refusé : Votre requête a été considérée comme automatisée.
action différente refusé : La vérification anti-robot ne correspond pas à ce formulaire.
jeton refusé par Google refusé : La vérification anti-robot a échoué.
service indisponible refusé : La vérification anti-robot est indisponible. Réessayez.Http::fake() mehrfach im selben Test aufzurufen, löscht die vorherigen Aufrufe nicht: Es antwortet die erste Übereinstimmung, und alle folgenden Fälle bekommen die Antwort des ersten. Deshalb steht vor jedem Fall ein Http::swap(new Factory()). Ohne das wird die ganze Serie grün, ohne irgendetwas zu prüfen.
Braucht es ein Paket?
anhskohbo/no-captcha, in der vorigen Fassung dieses Artikels erwähnt, gibt es weiterhin in Version 3.8. Es spart ein paar Zeilen, im Wesentlichen die Anzeige des v2-Widgets.
Der Code oben passt in eine Klasse und einen HTTP-Aufruf, ohne Abhängigkeit, die du pflegen müsstest, und lässt dir die Kontrolle über den Mindest-Score, die Prüfung der Action und die Fehlermeldungen. Für ein Formular ist das das bessere Verhältnis.
Wenn du bei null anfängst, ist das einen Blick wert: Cloudflare Turnstile erfüllt denselben Zweck mit einer Prüf-API gleicher Bauart. Nur die Prüf-URL und der Feldname ändern sich, der Aufbau der Regel bleibt identisch.
Häufige Fehler
required reicht es, das Feld nicht mitzuschicken, um das Captcha vollständig zu umgehen. Geprüft.env() null zurück. Nimm config('services.recaptcha.secret').Http::swap(new Factory()) wird die ganze Testreihe grün, ohne etwas zu prüfen.timeout() blockiert ein Ausfall der API die Anfrage des Nutzers bis zum Standardwert von PHP.

