
Não é preciso pacote: basta uma classe que implemente ValidationRule e interrogue https://www.google.com/recaptcha/api/siteverify com a fachada Http. Controla três coisas na resposta: success, a action esperada e o score. E associa sempre a regra ao required: o Laravel não executa uma regra personalizada sobre um campo vazio.
Um reCAPTCHA resume-se a duas coisas: um token produzido pelo navegador e a verificação desse token junto da Google a partir do teu servidor. O Laravel traz tudo o que é preciso para a segunda parte, o que torna o pacote de terceiros facultativo.
Artigo do percurso Desenvolvimento web. Este guia monta uma regra de validação autónoma, sem dependências, e põe-na à prova em seis cenários. Verificado em Laravel 13.30.1 com PHP 8.4.25.
Obter as chaves
Cria um site na consola do reCAPTCHA e escolhe a versão v3, que atribui um score de 0 a 1 sem pedir nada ao visitante. Ficas com duas chaves: a chave de site, pública, e a chave secreta, que nunca deve sair do servidor.
RECAPTCHA_KEY=votre_cle_de_site
RECAPTCHA_SECRET=votre_cle_secreteDeclara-as na configuração em vez de chamares env() a partir do código: assim que a configuração está em cache, o env() devolve null em todo o lado menos em config/.
'recaptcha' => [
'key' => env('RECAPTCHA_KEY'),
'secret' => env('RECAPTCHA_SECRET'),
],A regra de validação
Basta uma classe que implemente ValidationRule. Ela interroga a API de verificação e controla o sucesso, a ação esperada e o 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.');
}
}
}O controlo da ação merece uma explicação: sem ele, um token obtido numa página pública qualquer do teu site podia servir para passar a validação do formulário de contacto. O score sozinho não chega.
Ligar a regra ao formulário
O Laravel não executa uma regra personalizada sobre um valor vazio ou ausente. Verificado: só com a regra Recaptcha, um campo vazio passa a validação sem que parta um único pedido para a Google. Bastaria portanto não enviar o campo para contornar o captcha. A regra required não é opcional aqui.
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)],
]);
// …
}O formulário do lado do navegador
O formulário abaixo assenta nas regras de escape do Blade. A v3 não mostra nenhuma caixa a marcar: o script gera um token que é colocado num campo escondido mesmo antes do envio.
<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>A ação declarada aqui, contact, tem de ser exatamente a que a regra espera do lado do servidor.
Testar sem chamar a Google
Uma proteção que não se testa é uma proteção cujo estado se desconhece. Lembra-te também de desativar o modo de depuração em produção, sob pena de expores a tua chave secreta numa página de erro.
O Http::fake() permite simular cada resposta da 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()); // recomeça com um cliente novo em cada caso
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());
}Resultados obtidos:
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.Chamar o Http::fake() várias vezes no mesmo teste não apaga as anteriores: responde a primeira correspondência, e todos os casos seguintes recebem a resposta do primeiro. Daí o Http::swap(new Factory()) antes de cada caso. Sem ele, a série inteira fica verde sem verificar coisa nenhuma.
É preciso um pacote?
O anhskohbo/no-captcha, citado na versão anterior deste artigo, continua a existir na versão 3.8. Poupa umas linhas, essencialmente a apresentação do widget v2.
O código acima cabe numa classe e numa chamada HTTP, sem dependência para acompanhar, e deixa-te o controlo do score mínimo, do controlo da ação e das mensagens de erro. Num formulário, é a melhor relação.
A ter em conta se estás a partir do zero: o Cloudflare Turnstile cumpre o mesmo papel com uma API de verificação da mesma forma. Só mudam o URL de verificação e o nome do campo, a estrutura da regra fica igual.
Erros frequentes
required, basta não enviar o campo para contornar o captcha por completo. Verificado.env() devolve null. Passa por config('services.recaptcha.secret').Http::swap(new Factory()), toda a série de testes fica verde sem verificar nada.timeout(), uma avaria da API bloqueia o pedido do utilizador até ao prazo predefinido do PHP.

