En este tutorial vamos a ver cómo añadir Google reCAPTCHA a un formulario de Laravel utilizando Blade y Laracaptcha. Empezaremos por reCAPTCHA v2, la versión con la casilla de comprobación, y después veremos qué cambia al utilizar reCAPTCHA v3.
Contenidos
Vamos a construir una página de contacto que comprueba el captcha antes de mandar el mensaje. También conservaremos los datos introducidos cuando haya errores, porque bastante tiene uno con equivocarse en el email como para tener que escribir otra vez la consulta entera.
Introducción
Para seguir los pasos conviene conocer estas herramientas:
- PHP: Tutorial de PHP.
- HTML: Tutorial de HTML.
- Consola: Introducción a la línea de comandos.
Necesitarás PHP 8.3 o posterior y Composer. El formulario terminado tendrá este aspecto:

Instala las dependencias
Laracaptcha proporciona la regla de Laravel para verificar la respuesta de Google y un componente que podemos usar en las vistas Blade. Al seleccionar recaptcha_v2, el paquete se ocupa de cargar el widget correspondiente.
Puedes incorporarlo a una aplicación existente o comenzar con este proyecto de Laravel 13:
composer create-project laravel/laravel:^13.0 contacto-recaptcha
cd contacto-recaptcha
composer require edulazaro/laracaptcha:^1.2
Configura reCAPTCHA
Google facilita unas claves públicas para probar reCAPTCHA v2. El widget mostrará un aviso indicando que son de pruebas. Copia la siguiente configuración en .env; cuando pases al dominio real necesitarás tus propias claves:
CAPTCHA_DRIVER=recaptcha_v2
RECAPTCHA_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
CACHE_STORE=file
SESSION_DRIVER=file
MAIL_MAILER=log
MAIL_FROM_ADDRESS=web@example.com
MAIL_FROM_NAME="Mi aplicación"
CONTACT_RECIPIENT=contacto@example.com
El archivo de sesión y la caché nos permiten probar este formulario sin preparar una base de datos. Si vienes de otra configuración, ejecuta php artisan config:clear después de modificar las variables.
Prepara el correo de contacto
Nuestro destino será un correo de contacto. Separaremos su contenido del controlador mediante una clase Mailable y una vista de texto. Para seguir el tutorial basta con el transporte log: los mensajes aparecerán en el registro de Laravel y no saldrán hacia ninguna dirección real.
Laravel puede crear la clase del correo y su vista con esta instrucción:
php artisan make:mail ContactMessage --view=mail.contact
En la clase app/Mail/ContactMessage.php vamos a separar el remitente de la persona a la que responderemos. El mensaje se construirá de esta manera:
<?php
namespace App\Mail;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
class ContactMessage extends Mailable
{
public function __construct(
public string $senderName,
public string $senderEmail,
public string $messageBody,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Consulta desde la web',
replyTo: [new Address($this->senderEmail, $this->senderName)],
);
}
public function content(): Content
{
return new Content(text: 'mail.contact');
}
}
En resources/views/mail/contact.blade.php podemos presentar la consulta con este formato sencillo:
Nombre: {{ $senderName }}
Email: {{ $senderEmail }}
{{ $messageBody }}
El receptor del correo será una opción de config/contact.php. Crea ese archivo con el siguiente array:
<?php
return ['recipient' => env('CONTACT_RECIPIENT', 'contacto@example.com')];
No utilizaremos el email recibido como remitente del sitio. Al colocarlo en replyTo podremos responder sin suplantar el dominio del visitante.
Valida el formulario en Laravel
El controlador recibirá los campos y la respuesta de reCAPTCHA. Primero validará todo y, si Google acepta el token, preparará el correo. Una casilla marcada en el navegador no basta para autorizar el envío: la comprobación se hace aquí, en PHP.
Después de generar ContactController, podremos dejarlo así:
php artisan make:controller ContactController
<?php
namespace App\Http\Controllers;
use App\Mail\ContactMessage;
use EduLazaro\Laracaptcha\Facades\Captcha;
use EduLazaro\Laracaptcha\Rules\Captcha as CaptchaRule;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;
class ContactController extends Controller
{
public function create(): View
{
return view('contact');
}
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:254'],
'message' => ['required', 'string', 'min:10', 'max:3000'],
Captcha::responseField() => ['required', CaptchaRule::make(action: 'contact')],
]);
Mail::to(config('contact.recipient'))->send(new ContactMessage(
$data['name'], $data['email'], $data['message'],
));
return back()->with('status', 'Hemos recibido tu mensaje.');
}
}
Define las rutas
Vamos a exponer la página en /contacto. El POST permanecerá dentro de las rutas web para conservar la sesión y la protección CSRF. Añade estas definiciones a routes/web.php:
<?php
use App\Http\Controllers\ContactController;
use Illuminate\Support\Facades\Route;
Route::get('/contacto', [ContactController::class, 'create'])->name('contact.create');
Route::post('/contacto', [ContactController::class, 'store'])
->middleware('throttle:5,1')->name('contact.send');
Crea la vista Blade
Nos falta la pantalla que verá el usuario. En resources/views/contact.blade.php combinaremos los campos habituales con el componente de Laracaptcha. Fíjate en que los errores del captcha se muestran junto a la casilla, utilizando el nombre que devuelve el paquete:
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Formulario de contacto</title>
<link rel="stylesheet" href="{{ asset('contacto.css') }}">
</head>
<body>
<main>
<p class="eyebrow">reCAPTCHA · Laravel + Blade</p>
<h1>Escríbenos</h1>
<p>Envíanos tu consulta desde este formulario.</p>
@if (session('status'))
<p class="success" role="status">{{ session('status') }}</p>
@endif
<form method="post" action="{{ route('contact.send') }}">
@csrf
<label for="name">Nombre</label>
<input id="name" name="name" value="{{ old('name') }}" required maxlength="100" autocomplete="name">
@error('name') <p class="error">{{ $message }}</p> @enderror
<label for="email">Email</label>
<input id="email" name="email" type="email" value="{{ old('email') }}" required maxlength="254" autocomplete="email">
@error('email') <p class="error">{{ $message }}</p> @enderror
<label for="message">Mensaje</label>
<textarea id="message" name="message" required minlength="10" maxlength="3000">{{ old('message') }}</textarea>
@error('message') <p class="error">{{ $message }}</p> @enderror
<div class="captcha">
<x-laracaptcha::widget action="contact" language="es" />
@error(\EduLazaro\Laracaptcha\Facades\Captcha::responseField())
<p class="error">{{ $message }}</p>
@enderror
</div>
<button>Enviar mensaje</button>
</form>
</main>
</body>
</html>
En reCAPTCHA v2, Captcha::responseField() corresponde a g-recaptcha-response. La regla consulta a Google antes de continuar con el envío, y el captcha vuelve a cargarse cuando Laravel redirige al formulario.
Hemos indicado action="contact" para dejar preparada la variante v3. La versión v2 no comprueba esa acción. La validación de los campos y el límite de peticiones sí se aplican en ambas versiones.
Añade los estilos
Puedes aplicar estos estilos desde public/contacto.css. Mantienen la casilla de reCAPTCHA dentro del formulario también en pantallas estrechas y no requieren instalar un framework de CSS:
* { box-sizing: border-box; }
body { margin: 0; background: #f2f5f9; color: #172033; font: 16px/1.5 system-ui, sans-serif; }
main { width: min(560px, calc(100% - 32px)); margin: 40px auto; padding: 24px; background: white; border: 1px solid #dce3ed; border-radius: 16px; }
h1 { margin: 4px 0 12px; font-size: 28px; }
.eyebrow { color: #4255b5; font-weight: 700; font-size: 14px; }
label { display: block; margin: 16px 0 5px; font-weight: 600; }
input, textarea { width: 100%; padding: 10px; border: 1px solid #aab6c7; border-radius: 6px; font: inherit; }
textarea { min-height: 100px; resize: vertical; }
.captcha { margin: 20px 0; }
button { width: 100%; padding: 12px; color: white; background: #344caa; border: 0; border-radius: 7px; font: inherit; cursor: pointer; }
button:disabled { opacity: .6; cursor: wait; }
.error { color: #a31521; }
.success { padding: 10px; background: #e5f5eb; color: #195e35; }
@media (max-width: 420px) { main { width: calc(100% - 16px); padding: 12px; margin: 16px auto; } }
Prueba el formulario
Con el servidor en marcha, visita /contacto, marca la casilla de pruebas y manda una consulta. Después revisa storage/logs/laravel.log: debería aparecer el texto del correo. Si borras el token desde las herramientas del navegador, el servidor deberá rechazar el envío.
Inicia Laravel desde la consola:
php artisan serve
El contenido generado aparecerá al consultar este archivo:
tail -f storage/logs/laravel.log
Adapta la integración a reCAPTCHA v3
Para pasar a v3 debes registrar unas credenciales de esa versión. Las claves públicas que hemos utilizado en v2 no permiten probar sus puntuaciones. La configuración quedaría así:
CAPTCHA_DRIVER=recaptcha_v3
RECAPTCHA_V3_KEY=tu_clave_publica_v3
RECAPTCHA_V3_SECRET=tu_clave_secreta_v3
RECAPTCHA_V3_MIN_SCORE=0.5
La vista ya declara la acción contact y el servidor espera ese valor. El componente obtiene el token y lo renueva mientras la página permanece abierta; al enviar se comprobará en el servidor.
Puedes comenzar con 0.5 y revisarlo al disponer de tráfico real. En la guía de reCAPTCHA v3 encontrarás la explicación de puntuaciones y acciones. Nuestra regla comprobará contact en este proveedor.
Revisa la configuración del servidor
Al publicar deberás registrar tus claves y servir el formulario por HTTPS. Revisa también el correo. Laracaptcha permite limitar el hostname que devuelve Google con este ajuste:
CAPTCHA_HOSTNAMES=ejemplo.com,www.ejemplo.com
Añade tus dominios reales cuando reemplaces las credenciales de prueba. Google indica que la respuesta de reCAPTCHA dura dos minutos y solo puede verificarse una vez. Conserva la clave privada en el entorno de Laravel.
La cuenta de correo real se configura en MAIL_MAILER y las variables de su transporte. Cuando utilices colas, deja la verificación de reCAPTCHA antes de enviar el trabajo.
Comprueba la validación con tests
La integración de servidor se puede comprobar con una verificación simulada. Captcha::fake() acepta las respuestas y Captcha::fake(false) permite rechazarlas; el sitio publicado seguirá utilizando las comprobaciones de Google.
Para revisar el POST, genera esta clase de test:
php artisan make:test ContactFormTest --phpunit
Para revisar un envío válido, añade las importaciones al archivo y este método a la clase del test:
use App\Mail\ContactMessage;
use EduLazaro\Laracaptcha\Facades\Captcha;
use Illuminate\Support\Facades\Mail;
public function test_contact_form_sends_a_verified_message(): void
{
Captcha::fake();
Mail::fake();
$this->postJson('/contacto', [
'name' => 'Edu',
'email' => 'edu@example.com',
'message' => 'Una consulta de prueba para el formulario.',
Captcha::responseField() => 'test-token',
])->assertRedirect();
Mail::assertSent(ContactMessage::class);
}
El comando php artisan test --filter=ContactFormTest ejecutará la prueba. Añade escenarios donde el captcha falte, falle o se repita y comprueba que no sale ningún correo.
Finalizando
Con esto queda lista la integración de reCAPTCHA en Laravel y Blade. El mismo flujo sirve para una solicitud de acceso o una reserva: cambia los campos y la operación final, conservando la verificación antes de realizarla. Puedes empezar con v2 y valorar v3 cuando tengas tráfico con el que ajustar su puntuación.
-
Turnstile con Blade: Cómo integrar Turnstile en Laravel.
Esto ha sido todo. ¡Espero que te resulte útil!