
Pakiet nie jest potrzebny: wystarczy klasa implementująca ValidationRule, która odpytuje https://www.google.com/recaptcha/api/siteverify przez fasadę Http. Sprawdź w odpowiedzi trzy rzeczy: success, oczekiwaną action i score. I zawsze łącz regułę z required: Laravel nie uruchamia własnej reguły na pustym polu.
reCAPTCHA sprowadza się do dwóch rzeczy: tokenu wygenerowanego przez przeglądarkę i weryfikacji tego tokenu u Google z twojego serwera. Laravel daje wszystko, czego potrzeba do drugiej części, więc pakiet zewnętrzny staje się opcjonalny.
Artykuł ze ścieżki Programowanie webowe. Ten przewodnik buduje samodzielną regułę walidacji, bez żadnej zależności, i sprawdza ją na sześciu scenariuszach. Zweryfikowane na Laravelu 13.30.1 z PHP 8.4.25.
Zdobądź klucze
Utwórz witrynę w konsoli reCAPTCHA i wybierz wersję v3, która przyznaje wynik od 0 do 1, o nic nie pytając odwiedzającego. Dostajesz dwa klucze: klucz witryny, publiczny, oraz klucz tajny, który nigdy nie może opuścić serwera.
RECAPTCHA_KEY=votre_cle_de_site
RECAPTCHA_SECRET=votre_cle_secreteZadeklaruj je w konfiguracji, zamiast wywoływać env() z poziomu kodu: gdy konfiguracja trafi do cache’u, env() zwraca null wszędzie poza config/.
'recaptcha' => [
'key' => env('RECAPTCHA_KEY'),
'secret' => env('RECAPTCHA_SECRET'),
],Reguła walidacji
Wystarczy klasa implementująca ValidationRule. Odpytuje API weryfikacji i sprawdza sukces, oczekiwaną akcję oraz wynik.
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.');
}
}
}Kontrola akcji zasługuje na wyjaśnienie: bez niej token zdobyty na dowolnej publicznej podstronie twojej witryny mógłby przejść walidację formularza kontaktowego. Sam wynik nie wystarczy.
Podepnij regułę pod formularz
Laravel nie uruchamia własnej reguły na wartości pustej albo nieobecnej. Sprawdzone: z samą regułą Recaptcha puste pole przechodzi walidację i do Google nie leci żadne żądanie. Wystarczyłoby więc nie wysłać pola, żeby obejść captchę. Reguła required nie jest tu opcjonalna.
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)],
]);
// …
}Formularz po stronie przeglądarki
Poniższy formularz opiera się na zasadach escapowania w Blade. Wersja v3 nie wyświetla żadnego pola wyboru: skrypt generuje token, który tuż przed wysyłką trafia do ukrytego pola.
<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>Zadeklarowana tutaj akcja, contact, musi być dokładnie tą, której oczekuje reguła po stronie serwera.
Testowanie bez odpytywania Google
Ochrona, której się nie testuje, to ochrona o nieznanym stanie. Pamiętaj też, żeby wyłączyć tryb debugowania na produkcji, inaczej wystawisz klucz tajny na stronie błędu.
Http::fake() pozwala zasymulować każdą odpowiedź 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()); // przy każdym przypadku startuje z nowym klientem
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());
}Otrzymane wyniki:
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.Wywołanie Http::fake() kilka razy w tym samym teście nie kasuje poprzednich: odpowiada pierwsze dopasowanie, a wszystkie kolejne przypadki dostają odpowiedź z pierwszego. Stąd Http::swap(new Factory()) przed każdym przypadkiem. Bez tego cała seria świeci na zielono, nie sprawdzając niczego.
Czy potrzebny jest pakiet?
anhskohbo/no-captcha, przywoływany w poprzedniej wersji tego artykułu, wciąż istnieje w wersji 3.8. Oszczędza kilka linii, głównie na wyświetleniu widgetu v2.
Powyższy kod mieści się w jednej klasie i jednym wywołaniu HTTP, nie dokłada zależności do pilnowania i zostawia ci kontrolę nad minimalnym wynikiem, kontrolą akcji i komunikatami błędów. Przy formularzu to najlepszy układ.
Jeśli zaczynasz od zera, warto wiedzieć: Cloudflare Turnstile pełni tę samą rolę i ma API weryfikacji o tym samym kształcie. Zmienia się tylko adres weryfikacji i nazwa pola, struktura reguły zostaje ta sama.
Częste błędy
required wystarczy nie wysłać pola, żeby całkowicie obejść captchę. Sprawdzone.env() zwraca null. Korzystaj z config('services.recaptcha.secret').Http::swap(new Factory()) cała seria testów świeci na zielono, nie sprawdzając niczego.timeout() awaria API blokuje żądanie użytkownika aż do domyślnego limitu PHP.

