Los CAPTCHA tradicionales protegen los formularios contra bots, pero también frustran a usuarios legítimos con acertijos, selección de imágenes y una accesibilidad deficiente. Google reCAPTCHA v2 es famoso por hacer que los usuarios hagan clic en semáforos hasta que abandonan la página por frustración. Incluso los CAPTCHA «invisibles» suelen fallar en silencio o degradar la experiencia de usuario.
Kits de inicio de alta calidad con flujo de autenticación integrada (Auth.js), carga de objetos (AWS, Clouflare R2, Firebase Storage, Supabase Storage), pagos integrados (Stripe, LemonSqueezy), flujo de verificación de correo electrónico (Resend, Postmark, Sendgrid) y mucho más . Compatible con cualquier base de datos (Redis, Postgres, MongoDB, SQLite, Firestore).
Get all 3 kits Bundle ↗- Next.js Starter Kit
- SvelteKit Starter Kit
- Astro Starter Kit
One-time license · Lifetime updates
Cloudflare Turnstile sustituye los CAPTCHA por una detección inteligente de bots que prioriza la privacidad. Se ejecuta en segundo plano, analizando el comportamiento del navegador y las huellas del dispositivo sin pedir a los usuarios que resuelvan acertijos. La mayoría de los usuarios legítimos nunca ven un desafío. Cuando aparece, es una casilla de verificación sencilla (no una galería de imágenes de hidrantes).
En esta guía, integrará Turnstile en una aplicación Astro para proteger formularios de registro, formularios de contacto y endpoints de API. Añadirá el widget a sus formularios, verificará los tokens en el servidor y gestionará casos límite como la limitación de tasa y los estados de error. Al final, tendrá una protección contra bots lista para producción que escala globalmente en la red de Cloudflare.
Requisitos previos
Necesitará lo siguiente:
- Node.js 20 o posterior
- Una cuenta de Cloudflare
Descripción general de la arquitectura
1. Frontend (componente Astro con widget Turnstile) → Renderiza el widget Turnstile en el formulario → Captura el token cuando el usuario completa el desafío
2. Backend (ruta de API o manejador de formulario) → Recibe los datos del formulario + token Turnstile → Verifica el token con la API de Cloudflare → Procesa la solicitud solo si la verificación tiene éxito
3. API de Cloudflare Turnstile → Valida el token → Devuelve éxito/fallo + metadatosObtener las claves de API de Turnstile
Antes de construir, necesita las claves de API de Turnstile:
- Vaya al panel de Cloudflare
- Navegue a Application security > Turnstile en la barra lateral
- Haga clic en Add site
- Configure su sitio:
- Site name:
my-astro-turnstile-app - Domain: Añada su dominio (por ejemplo, workers.dev)
- Widget Mode: Elija Managed (recomendado para la mayoría de casos de uso)
- Site name:
- Haga clic en Create
Recibirá:
- Site Key (pública, se usa en el frontend)
- Secret Key (privada, se usa en el backend)
Guarde estas claves; las necesitará en los siguientes pasos.
Crear una nueva aplicación Astro
Comencemos creando un nuevo proyecto Astro. Ejecute el siguiente comando:
npm create astro@latest my-astro-turnstile-appcd my-astro-turnstile-appnpm install wranglerCuando se le solicite, elija:
Use minimal (empty) templatecuando se le pregunte cómo iniciar el nuevo proyecto.Yescuando se le pregunte si desea instalar dependencias.Yescuando se le pregunte si desea inicializar un repositorio git.
Una vez hecho esto, puede moverse al directorio del proyecto e iniciar la aplicación:
npm run devLa aplicación debería estar ejecutándose en localhost:4321.
Integrar el adaptador de Cloudflare en su proyecto Astro
Para desplegar su proyecto Astro en Cloudflare Workers y usar variables de entorno de forma segura, necesita instalar el adaptador de Cloudflare. Ejecute el comando siguiente:
npx astro add cloudflare --yesActualice astro.config.mjs para que output sea server:
import { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ output: 'server', adapter: cloudflare()});Configurar variables de entorno
Añada sus claves de Turnstile a wrangler.jsonc:
{ "main": "dist/_worker.js/index.js", "name": "my-astro-turnstile-app", "compatibility_date": "2025-12-08", "compatibility_flags": [ "nodejs_compat", "global_fetch_strictly_public" ], "assets": { "binding": "ASSETS", "directory": "./dist" }, "observability": { "enabled": true }, "vars": { "TURNSTILE_SITE_KEY": "...", "TURNSTILE_SECRET_KEY": "..." }}Importante: Nunca suba su clave secreta al control de versiones. Para el desarrollo local, cree un archivo
.dev.vars:
TURNSTILE_SITE_KEY="your-site-key"TURNSTILE_SECRET_KEY="your-secret-key"Para producción, configure el secreto mediante el panel de Cloudflare o la CLI:
npx wrangler secret put TURNSTILE_SITE_KEYnpx wrangler secret put TURNSTILE_SECRET_KEYAñadir tipos de TypeScript
Cree src/env.d.ts para añadir los tipos adecuados para las variables de entorno:
/// <reference types="astro/client" />
type ENV = { TURNSTILE_SITE_KEY: string TURNSTILE_SECRET_KEY: string}
type Runtime = import('@astrojs/cloudflare').Runtime<ENV>
declare namespace App { interface Locals extends Runtime { }}Esto le proporciona seguridad de tipos completa al acceder a locals.runtime.env.TURNSTILE_SITE_KEY y locals.runtime.env.TURNSTILE_SECRET_KEY en sus rutas.
Crear una utilidad de verificación
Para reutilización, extraiga la lógica de verificación de Turnstile en una función de utilidad. Cree src/lib/turnstile.ts:
interface TurnstileResponse { success: boolean challenge_ts?: string hostname?: string 'error-codes'?: string[] action?: string cdata?: string}
/** * Verify a Turnstile token with Cloudflare's API */export async function verifyTurnstileToken( token: string, secretKey: string, remoteIP?: string): Promise<TurnstileResponse> { const response = await fetch( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ secret: secretKey, response: token, remoteip: remoteIP, // Optional: pass client IP for additional validation }), } )
if (!response.ok) { throw new Error(`Turnstile API error: ${response.status}`) }
return response.json()}
/** * Extract Turnstile token from form data or request body */export function getTurnstileToken(formData: FormData): string | null { return formData.get('cf-turnstile-response') as string | null}
/** * Helper to check if Turnstile verification is required */export function isTurnstileEnabled(env: any): boolean { return !!(env.TURNSTILE_SITE_KEY && env.TURNSTILE_SECRET_KEY)}Ahora puede usar esta utilidad en cualquier manejador de formulario o ruta de API:
import { verifyTurnstileToken, getTurnstileToken } from '../lib/turnstile'
const token = getTurnstileToken(formData)if (!token) { return new Response('Missing verification token', { status: 400 })}
const result = await verifyTurnstileToken( token, Astro.locals.runtime.env.TURNSTILE_SECRET_KEY)
if (!result.success) { return new Response('Verification failed', { status: 403 })}
// Proceed with form processing...Proteger endpoints de API
También puede proteger endpoints de API con Turnstile. Esto es útil para prevenir el abuso automatizado de APIs públicas.
Cree src/pages/api/contact.ts:
import type { APIRoute } from 'astro'import { verifyTurnstileToken } from '../../lib/turnstile'
export const POST: APIRoute = async ({ request, locals }) => { try { const body = await request.json() const { email, message, token } = body
// Validate required fields if (!email || !message) { return new Response( JSON.stringify({ error: 'Email and message are required' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ) }
// Verify Turnstile token if (!token) { return new Response( JSON.stringify({ error: 'Verification token missing' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ) }
const secretKey = locals.runtime.env.TURNSTILE_SECRET_KEY const clientIP = request.headers.get('CF-Connecting-IP') || undefined
const verification = await verifyTurnstileToken(token, secretKey, clientIP)
if (!verification.success) { return new Response( JSON.stringify({ error: 'Verification failed', codes: verification['error-codes'], }), { status: 403, headers: { 'Content-Type': 'application/json' } } ) }
// Process the contact form (e.g., send email, save to database) console.log('Valid contact form submission:', email)
return new Response( JSON.stringify({ success: true, message: 'Message received' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ) } catch (error) { console.error('Contact API error:', error) return new Response( JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ) }}Formulario frontend que llama a esta API:
---const siteKey = import.meta.env.PROD ? Astro.locals.runtime.env.TURNSTILE_SITE_KEY : import.meta.env.TURNSTILE_SITE_KEY;---
<html lang="en"> <head> <meta charset="UTF-8" /> <title>Contact Us</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> </head> <body> <h1>Contact Us</h1> <form id="contact-form"> <input type="email" name="email" placeholder="Your email" required /> <textarea name="message" placeholder="Your message" required></textarea> <div class="cf-turnstile" data-sitekey={siteKey}></div> <button type="submit">Send Message</button> </form> <script> const form = document.getElementById('contact-form'); form?.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); const token = formData.get('cf-turnstile-response'); const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, email: formData.get('email'), message: formData.get('message'), }), }); const result = await response.json(); if (result.success) { alert('Message sent successfully!'); form.reset(); } else alert('Error: ' + result.error); }); </script> </body></html>Personalización del widget
Turnstile ofrece varios modos de widget y temas:
Modos de widget
<!-- Managed (recommended): Shows challenge only when needed --><div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<!-- Non-Interactive: Invisible, always runs in background --><div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-appearance="interaction-only"></div>Callbacks
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onTurnstileSuccess" data-error-callback="onTurnstileError" data-expired-callback="onTurnstileExpired"></div>
<script> function onTurnstileSuccess(token) { console.log('Turnstile verified:', token); }
function onTurnstileError(error) { console.error('Turnstile error:', error); }
function onTurnstileExpired() { console.warn('Turnstile token expired'); }</script>Manejo de errores
Códigos de error comunes de Turnstile:
| Código de error | Significado | Solución |
|---|---|---|
missing-input-secret | No se proporcionó la clave secreta | Compruebe las variables de entorno |
invalid-input-secret | Clave secreta no válida | Verifique su clave secreta |
missing-input-response | No se proporcionó el token | Asegúrese de que el formulario envíe el token |
invalid-input-response | Token no válido o caducado | Pida al usuario que reintente la verificación |
timeout-or-duplicate | Token ya usado o caducado | Genere un token nuevo |
Gestione los errores con elegancia:
const verification = await verifyTurnstileToken(token, secretKey)
if (!verification.success) { const errorCode = verification['error-codes']?.[0] switch (errorCode) { case 'timeout-or-duplicate': return new Response('Token expired. Please refresh the page.', { status: 400 }) case 'invalid-input-response': return new Response('Invalid verification. Please try again.', { status: 400 }) default: return new Response('Verification failed. Please contact support.', { status: 500 }) }}Desplegar en Cloudflare Workers
Despliegue su aplicación protegida contra bots en producción:
# Build the projectnpm run build
# Deploy to Cloudflare Workersnpx wrangler deploySus formularios ya están protegidos en producción.
Buenas prácticas
- Verifique siempre en el servidor: Nunca confíe únicamente en la validación del lado del cliente.
- Use el modo managed: Deje que Turnstile decida cuándo mostrar desafíos.
- Gestione los errores con elegancia: Muestre mensajes comprensibles para el usuario, no códigos de error sin procesar.
- Combine con limitación de tasa: Añada limitación de tasa para una protección adicional.
Conclusión
Cloudflare Turnstile sustituye los CAPTCHA frustrantes por una protección invisible contra bots que prioriza la privacidad. Al integrar Turnstile en sus formularios y endpoints de API de Astro, bloquea el abuso automatizado sin degradar la experiencia de usuario para visitantes legítimos.
El patrón que ha construido — widget en el cliente + verificación en el servidor + manejo elegante de errores — escala fácilmente a cualquier formulario o API. Añádalo a páginas de inicio de sesión, flujos de pago, secciones de comentarios o cualquier endpoint que necesite protección contra bots.
Si tiene preguntas o comentarios, no dude en contactarme en Twitter.