Protección contra bots en Astro con Cloudflare Turnstile
LaunchFast Logo LaunchFast
Blog
2349 palabras 12 min de lectura

Protección contra bots en Astro con Cloudflare Turnstile

Sustituya CAPTCHA por detección invisible de bots en Astro usando Cloudflare Turnstile. Proteja formularios de registro, páginas de inicio de sesión y endpoints de API contra bots sin frustrar a usuarios reales.

Rishi Raj Jain
Rishi Raj Jain Autor
Protección contra bots en Astro con Cloudflare Turnstile

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.

Sponsored

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 ↗

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:

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 + metadatos

Obtener las claves de API de Turnstile

Antes de construir, necesita las claves de API de Turnstile:

  1. Vaya al panel de Cloudflare
  2. Navegue a Application security > Turnstile en la barra lateral
  3. Haga clic en Add site
  4. 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)
  5. 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:

Terminal window
npm create astro@latest my-astro-turnstile-app
cd my-astro-turnstile-app
npm install wrangler

Cuando se le solicite, elija:

  • Use minimal (empty) template cuando se le pregunte cómo iniciar el nuevo proyecto.
  • Yes cuando se le pregunte si desea instalar dependencias.
  • Yes cuando se le pregunte si desea inicializar un repositorio git.

Una vez hecho esto, puede moverse al directorio del proyecto e iniciar la aplicación:

Terminal window
npm run dev

La 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:

Terminal window
npx astro add cloudflare --yes

Actualice astro.config.mjs para que output sea server:

astro.config.mjs
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:

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:

.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:

Terminal window
npx wrangler secret put TURNSTILE_SITE_KEY
npx wrangler secret put TURNSTILE_SECRET_KEY

Añadir tipos de TypeScript

Cree src/env.d.ts para añadir los tipos adecuados para las variables de entorno:

src/env.d.ts
/// <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:

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:

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:

src/pages/contact.astro
---
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 errorSignificadoSolución
missing-input-secretNo se proporcionó la clave secretaCompruebe las variables de entorno
invalid-input-secretClave secreta no válidaVerifique su clave secreta
missing-input-responseNo se proporcionó el tokenAsegúrese de que el formulario envíe el token
invalid-input-responseToken no válido o caducadoPida al usuario que reintente la verificación
timeout-or-duplicateToken ya usado o caducadoGenere 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:

Terminal window
# Build the project
npm run build
# Deploy to Cloudflare Workers
npx wrangler deploy

Sus 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.

Sigue leyendo