
Je hebt geen package nodig: een klasse die ValidationRule implementeert en met de facade Http naar https://www.google.com/recaptcha/api/siteverify gaat, volstaat. Controleer drie dingen in het antwoord: success, de verwachte action en de score. En koppel de regel altijd aan required: Laravel voert een eigen regel niet uit op een leeg veld.
Een reCAPTCHA komt neer op twee dingen: een token dat de browser aanmaakt, en een controle van dat token bij Google vanaf je server. Voor dat tweede deel levert Laravel alles wat je nodig hebt, en daarmee wordt een externe package optioneel.
Artikel uit het leertraject Webontwikkeling. Deze gids bouwt een zelfstandige validatieregel zonder dependency en zet die onder druk in zes scenario’s. Gecontroleerd op Laravel 13.30.1 met PHP 8.4.25.
De sleutels ophalen
Maak een site aan in de reCAPTCHA-console en kies versie v3, die een score van 0 tot 1 toekent zonder de bezoeker iets te vragen. Je krijgt twee sleutels: de sitesleutel, die publiek is, en de geheime sleutel, die de server nooit mag verlaten.
RECAPTCHA_KEY=votre_cle_de_site
RECAPTCHA_SECRET=votre_cle_secreteZet ze in de configuratie in plaats van env() vanuit je code aan te roepen: zodra de configuratie gecachet is, geeft env() overal buiten config/ gewoon null terug.
'recaptcha' => [
'key' => env('RECAPTCHA_KEY'),
'secret' => env('RECAPTCHA_SECRET'),
],De validatieregel
Eén klasse die ValidationRule implementeert volstaat. Ze bevraagt de verificatie-API en controleert het succes, de verwachte actie en de 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.');
}
}
}De controle op de actie verdient een woordje uitleg: zonder die controle zou een token dat op een willekeurige publieke pagina van je site is opgehaald, de validatie van het contactformulier passeren. De score alleen is niet genoeg.
De regel op het formulier aansluiten
Laravel voert een eigen validatieregel niet uit op een lege of ontbrekende waarde. Geverifieerd: met alleen de regel Recaptcha passeert een leeg veld de validatie zonder dat er ook maar één request naar Google vertrekt. Het veld gewoon niet meesturen volstaat dus om de captcha te omzeilen. De regel required is hier niet optioneel.
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)],
]);
// …
}Het formulier in de browser
Het formulier hieronder steunt op de escaperegels van Blade. Bij v3 verschijnt er geen enkel vinkje: het script genereert een token dat je vlak voor het versturen in een verborgen veld zet.
<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>De actie die je hier opgeeft, contact, moet exact overeenkomen met wat de regel op de server verwacht.
Testen zonder Google aan te roepen
Een bescherming die je niet test, is een bescherming waarvan je de staat niet kent. Denk er ook aan om de debugmodus in productie uit te zetten, anders lekt je geheime sleutel via een foutpagina naar buiten.
Met Http::fake() simuleer je elk antwoord van de 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()); // begint bij elk geval met een nieuwe 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());
}De resultaten:
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() meerdere keren aanroepen in dezelfde test wist de vorige aanroepen niet: de eerste match antwoordt, en alle volgende gevallen krijgen datzelfde antwoord. Vandaar de Http::swap(new Factory()) vóór elk geval. Zonder die regel kleurt de hele reeks groen zonder iets te controleren.
Heb je een package nodig?
anhskohbo/no-captcha, genoemd in de vorige versie van dit artikel, bestaat nog steeds in versie 3.8. Het scheelt je een paar regels, in hoofdzaak het tonen van de v2-widget.
De code hierboven past in één klasse en één HTTP-aanroep, zonder dependency om op te volgen, en laat jou de minimale score, de actiecontrole en de foutmeldingen bepalen. Voor één formulier is dat de beste verhouding.
Begin je van nul, let dan op het volgende: Cloudflare Turnstile vervult dezelfde rol met een verificatie-API van dezelfde vorm. Alleen de verificatie-URL en de veldnaam verschillen, de structuur van de regel blijft identiek.
Veelgemaakte fouten
required volstaat het het veld niet mee te sturen om de captcha volledig te omzeilen. Geverifieerd.env() null terug. Ga via config('services.recaptcha.secret').Http::swap(new Factory()) kleurt de hele testreeks groen zonder iets te controleren.timeout() blokkeert een storing van de API het request van je gebruiker tot de standaardtijd van PHP verstreken is.

