mcpField
C
cloudflare-turnstile

cloudflare-turnstile

github.com/jackspace安全1642 次安装12 周前更新
代码仓库

关于 cloudflare-turnstile

用于将 Cloudflare Turnstile 集成到网页表单和应用中的开发技能,涵盖组件配置、客户端渲染、服务端令牌验证、框架示例、测试与故障排查。

主要功能

  1. 01配置 Turnstile 组件
  2. 02在表单和 SPA 中渲染组件
  3. 03在服务端验证令牌
  4. 04采用 React 和 Cloudflare Workers 示例
  5. 05使用测试密钥
  6. 06排查错误和 CSP 问题

适用场景

  1. 01为网页表单添加机器人验证
  2. 02在 React 或 Next.js 应用中集成 Turnstile
  3. 03在后端验证提交的令牌
  4. 04在自动化测试中检查组件行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-turnstile

安装后在 Claude Code 中输入/cloudflare-turnstile确认已加载。

附带文件README.mdSKILL.json

详细介绍

Cloudflare Turnstile

状态:生产就绪 最后更新:2025-10-22 依赖项:无(可选:React 可使用 @marsidev/react-turnstile) 最新版本:@marsidev/[email protected]、[email protected]


快速开始(10 分钟)

1. 创建 Turnstile 小组件

从 Cloudflare Dashboard 获取 sitekey 和 secret key。

# Navigate to: https://dash.cloudflare.com/?to=/:account/turnstile
# Create new widget → Copy sitekey (public) and secret key (private)

为何重要:

  • 每个小组件都有唯一的 sitekey/secret 密钥对
  • sitekey 放在前端(公开)
  • secret key 只能放在后端(私密)
  • 为开发、预发布和生产环境使用不同的小组件
2. 在前端添加小组件

将 Turnstile 小组件嵌入 HTML 表单。

<!DOCTYPE html>
<html>
<head>
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
  <form id="myForm" action="/submit" method="POST">
    <input type="email" name="email" required>
    
    <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
    <button type="submit">Submit</button>
  </form>
</body>
</html>

重要:

  • 切勿代理或缓存 api.js——必须从 Cloudflare CDN 加载
  • 小组件会自动创建隐藏输入项 cf-turnstile-response,其中包含令牌
  • 令牌会在 5 分钟后过期
  • 每个令牌只能使用一次
3. 在服务器上验证令牌

务必在服务器端验证令牌。仅进行客户端验证并不安全。

// Cloudflare Workers example
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const formData = await request.formData()
    const token = formData.get('cf-turnstile-response')
    const ip = request.headers.get('CF-Connecting-IP')

    // Validate token with Siteverify API
    const verifyFormData = new FormData()
    verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
    verifyFormData.append('response', token)
    verifyFormData.append('remoteip', ip)

    const result = await fetch(
      'https://challenges.cloudflare.com/turnstile/v0/siteverify',
      {
        method: 'POST',
        body: verifyFormData,
      }
    )

    const outcome = await result.json()

    if (!outcome.success) {
      return new Response('Invalid Turnstile token', { status: 401 })
    }

    // Token valid - proceed with form processing
    return new Response('Success!')
  }
}

3 步设置流程

第 1 步:创建小组件配置
  1. 登录 Cloudflare Dashboard
  2. 前往 Turnstile 页面
  3. 点击“Add Site”
  4. 配置:
    • 小组件模式:Managed(推荐)、Non-Interactive 或 Invisible
    • 域名:添加允许的主机名(例如,example.com;开发时可添加 localhost)
    • 名称:填写描述性名称(例如,“生产环境登录表单”)

要点:

  • 为开发、预发布和生产环境使用不同的小组件
  • 仅将你控制的域名加入允许列表
  • Managed 模式能在安全性和用户体验之间取得最佳平衡
  • 本地测试时必须明确添加 localhost
第 2 步:客户端集成

选择隐式或显式渲染:

隐式渲染(推荐用于静态表单):


<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>


<div class="cf-turnstile"
     data-sitekey="YOUR_SITE_KEY"
     data-callback="onSuccess"
     data-error-callback="onError"></div>

<script>
function onSuccess(token) {
  console.log('Turnstile success:', token)
}

function onError(error) {
  console.error('Turnstile error:', error)
}
</script>

显式渲染(用于 SPA/动态 UI):

// 1. Load script with explicit mode
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>

// 2. Render programmatically
const widgetId = turnstile.render('#container', {
  sitekey: 'YOUR_SITE_KEY',
  callback: (token) => {
    console.log('Token:', token)
  },
  'error-callback': (error) => {
    console.error('Error:', error)
  },
  theme: 'auto',
  execution: 'render', // or 'execute' for manual trigger
})

// Control lifecycle
turnstile.reset(widgetId)        // Reset widget
turnstile.remove(widgetId)       // Remove widget
turnstile.execute(widgetId)      // Manually trigger challenge
const token = turnstile.getResponse(widgetId) // Get current token

React 集成(使用 @marsidev/react-turnstile):

import { Turnstile } from '@marsidev/react-turnstile'

export function MyForm() {
  const [token, setToken] = useState<string>()

  return (
    <form>
      <Turnstile
        siteKey={TURNSTILE_SITE_KEY}
        onSuccess={setToken}
        onError={(error) => console.error(error)}
      />
      <button disabled={!token}>Submit</button>
    </form>
  )
}
第 3 步:服务器端验证

必须:始终调用 Siteverify API 验证令牌。

interface TurnstileResponse {
  success: boolean
  challenge_ts?: string
  hostname?: string
  error-codes?: string[]
  action?: string
  cdata?: string
}

async function validateTurnstile(
  token: string,
  secretKey: string,
  options?: {
    remoteip?: string
    idempotency_key?: string
    expectedAction?: string
    expectedHostname?: string
  }
): Promise<TurnstileResponse> {
  const formData = new FormData()
  formData.append('secret', secretKey)
  formData.append('response', token)

  if (options?.remoteip) {
    formData.append('remoteip', options.remoteip)
  }

  if (options?.idempotency_key) {
    formData.append('idempotency_key', options.idempotency_key)
  }

  const response = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: formData,
    }
  )

  const result = await response.json<TurnstileResponse>()

  // Additional validation
  if (result.success) {
    if (options?.expectedAction && result.action !== options.expectedAction) {
      return { success: false, 'error-codes': ['action-mismatch'] }
    }

    if (options?.expectedHostname && result.hostname !== options.expectedHostname) {
      return { success: false, 'error-codes': ['hostname-mismatch'] }
    }
  }

  return result
}

// Usage in Cloudflare Worker
const result = await validateTurnstile(
  token,
  env.TURNSTILE_SECRET_KEY,
  {
    remoteip: request.headers.get('CF-Connecting-IP'),
    expectedHostname: 'example.com',
  }
)

if (!result.success) {
  return new Response('Turnstile validation failed', { status: 401 })
}

重要规则

务必做到

✅ 调用 Siteverify API——必须进行服务器端验证 ✅ 使用 HTTPS——切勿通过 HTTP 验证 ✅ 保护 secret key——切勿在前端代码中暴露 ✅ 处理令牌过期——令牌会在 5 分钟后过期 ✅ 实现错误回调——妥善处理失败情况 ✅ 使用测试用密钥——测试 sitekey:1x00000000000000000000AA ✅ 设置合理的超时时间——不要无限期等待验证结果 ✅ 验证 action/hostname——如有指定,请检查其他字段 ✅ 定期轮换密钥——通过 Dashboard 或 API 轮换密钥 ✅ 监控分析数据——跟踪通过率和失败情况

切勿这样做

❌ 跳过服务器验证——仅进行客户端验证会造成安全漏洞 ❌ 代理 api.js 脚本——必须从 Cloudflare CDN 加载 ❌ 重复使用令牌——每个令牌只能使用一次 ❌ 使用 GET 请求——Siteverify 只接受 POST ❌ 暴露 secret key——仅在后端环境中保存密钥 ❌ 信任客户端验证——令牌可能是伪造的 ❌ 缓存 api.js——未来的更新会导致集成失效 ❌ 在测试中使用生产密钥——请改用测试用密钥 ❌ 忽略错误回调——务必处理失败情况


预防已知问题

此技能可预防 12 个已记录的问题:

问题 #1:缺少服务器端验证

错误:Turnstile Analytics Dashboard 中没有任何令牌验证记录 来源:https://developers.cloudflare.com/turnstile/get-started/ 原因:开发者只实现了客户端小组件,跳过 Siteverify 调用 预防措施:所有模板都包含使用 Siteverify API 进行服务器端验证的强制步骤

问题 #2:令牌过期(5 分钟)

错误:延迟提交的有效令牌返回 success: false 来源:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因:令牌生成 300 秒后过期 预防措施:模板会说明有效期,并在令牌过期时实现令牌刷新

问题 #3:前端暴露密钥

错误:安全漏洞——攻击者可以验证自己的令牌 来源:https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因:密钥被硬编码在 JavaScript 中或在源代码中可见 预防措施:所有模板均展示仅在后端进行验证,并使用环境变量

问题 #4:向 Siteverify 发送 GET 请求

错误:API 返回 405 Method Not Allowed 来源:https://developers.cloudflare.com/turnstile/migration/recaptcha 原因:reCAPTCHA 支持 GET,Turnstile 要求使用 POST 预防措施:模板使用 POST,并通过 FormData 或 JSON 请求体发送数据

问题 #5:内容安全策略阻止加载

错误:错误 200500——“Loading error: The iframe could not be loaded” 来源:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:CSP 阻止了 challenges.cloudflare.com iframe 预防措施:技能包含 CSP 配置参考和 check-csp.sh 脚本

问题 #6:组件崩溃(错误 300030)

错误:合法用户遇到通用客户端执行错误 来源:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因:未知——似乎是 Cloudflare 端的问题(2025 年) 预防措施:模板实现错误回调、重试逻辑和回退处理

问题 #7:配置错误(错误 600010)

错误:组件因“配置错误”而失败 来源:https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 原因:组件配置中缺少或已删除主机名 预防措施:模板说明主机名允许列表要求和验证步骤

问题 #8:Safari 18 / macOS 15“隐藏 IP”问题

错误:启用 Safari“隐藏 IP 地址”时出现错误 300010 来源:https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因:隐私设置会干扰挑战信号 预防措施:错误处理参考文档说明了 Safari 的解决方法(关闭“隐藏 IP”)

问题 #9:Brave 浏览器彩纸动画失败

错误:成功动画期间验证失败 来源:https://github.com/brave/brave-browser/issues/45608 (April 2025) 原因:Brave Shields 会阻止动画脚本 预防措施:模板会在动画完成前处理成功状态

问题 #10:Next.js 与 Jest 不兼容

错误:@marsidev/react-turnstile 会导致 Jest 测试失败 来源:https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025) 原因:Jest 的模块解析问题 预防措施:测试指南包含 Jest 模拟模式和虚拟 sitekey 的用法

问题 #11:localhost 不在允许列表中

错误:错误 110200——“Unknown domain: Domain not allowed” 来源:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:开发环境使用生产组件,但允许列表中未包含 localhost 预防措施:模板在开发环境中使用虚拟测试密钥,并说明 localhost 允许列表要求

问题 #12:尝试重复使用令牌

错误:success: false,并显示“token already spent”错误 来源:https://developers.cloudflare.com/turnstile/troubleshooting/testing 原因:每个令牌只能验证一次 预防措施:模板说明单次使用限制和令牌刷新模式


配置文件参考

wrangler.jsonc(Cloudflare Workers)
{
  "name": "my-app",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-22",

  // Public sitekey (safe to commit)
  "vars": {
    "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" // Use real key in production
  },

  // Secret key (DO NOT commit - use wrangler secret)
  // Run: wrangler secret put TURNSTILE_SECRET_KEY
  "secrets": ["TURNSTILE_SECRET_KEY"]
}

这些设置的原因:

  • vars 用于公开的 sitekey(在客户端代码中可见)
  • secrets 用于私有的 secret key(已加密,仅限后端使用)
  • 开发时使用虚拟密钥(参见 testing-guide.md)
  • 每季度轮换生产环境的 secret key
必需的 CSP 指令
<meta http-equiv="Content-Security-Policy" content="
  script-src 'self' https://challenges.cloudflare.com;
  frame-src 'self' https://challenges.cloudflare.com;
  connect-src 'self' https://challenges.cloudflare.com;
">

常见模式

模式 1:Hono + Cloudflare Workers
import { Hono } from 'hono'

type Bindings = {
  TURNSTILE_SECRET_KEY: string
  TURNSTILE_SITE_KEY: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.post('/api/login', async (c) => {
  const body = await c.req.formData()
  const token = body.get('cf-turnstile-response')

  if (!token) {
    return c.text('Missing Turnstile token', 400)
  }

  // Validate token
  const verifyFormData = new FormData()
  verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
  verifyFormData.append('response', token.toString())
  verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '')

  const verifyResult = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: verifyFormData,
    }
  )

  const outcome = await verifyResult.json<{ success: boolean }>()

  if (!outcome.success) {
    return c.text('Invalid Turnstile token', 401)
  }

  // Process login
  return c.json({ message: 'Login successful' })
})

export default app

适用场景:在 Cloudflare Workers 中使用 Hono 框架的 API 路由

模式 2:React + Next.js App Router
'use client'

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export function ContactForm() {
  const [token, setToken] = useState<string>()
  const [error, setError] = useState<string>()

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()

    if (!token) {
      setError('Please complete the challenge')
      return
    }

    const formData = new FormData(e.currentTarget)
    formData.append('cf-turnstile-response', token)

    const response = await fetch('/api/contact', {
      method: 'POST',
      body: formData,
    })

    if (!response.ok) {
      setError('Submission failed')
      return
    }

    // Success
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />

      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setToken}
        onError={() => setError('Challenge failed')}
        onExpire={() => setToken(undefined)}
      />

      {error && <div className="error">{error}</div>}

      <button type="submit" disabled={!token}>
        Submit
      </button>
    </form>
  )
}

适用场景:在 Next.js 中使用 React hooks 的客户端表单

模式 3:使用虚拟密钥进行 E2E 测试
// test/helpers/turnstile.ts
export const TEST_TURNSTILE = {
  sitekey: {
    alwaysPass: '1x00000000000000000000AA',
    alwaysBlock: '2x00000000000000000000AB',
    invisible: '1x00000000000000000000BB',
    interactive: '3x00000000000000000000FF',
  },
  secretKey: {
    alwaysPass: '1x0000000000000000000000000000000AA',
    alwaysFail: '2x0000000000000000000000000000000AA',
    tokenSpent: '3x0000000000000000000000000000000AA',
  },
  dummyToken: 'XXXX.DUMMY.TOKEN.XXXX',
}

// Playwright test example
test('form submission with Turnstile', async ({ page }) => {
  // Set test environment
  await page.goto('/contact?test=true')

  // Widget uses test sitekey in test mode
  await page.fill('input[name="email"]', '[email protected]')

  // Turnstile auto-solves with dummy token
  await page.click('button[type="submit"]')

  await expect(page.locator('.success')).toBeVisible()
})

适用场景:自动化测试(Playwright、Cypress、Jest)

模式 4:小组件生命周期管理
class TurnstileManager {
  private widgetId: string | null = null
  private sitekey: string

  constructor(sitekey: string) {
    this.sitekey = sitekey
  }

  render(containerId: string, callbacks: {
    onSuccess: (token: string) => void
    onError: (error: string) => void
  }) {
    if (this.widgetId !== null) {
      this.reset() // Reset if already rendered
    }

    this.widgetId = turnstile.render(containerId, {
      sitekey: this.sitekey,
      callback: callbacks.onSuccess,
      'error-callback': callbacks.onError,
      'expired-callback': () => this.reset(),
    })

    return this.widgetId
  }

  reset() {
    if (this.widgetId !== null) {
      turnstile.reset(this.widgetId)
    }
  }

  remove() {
    if (this.widgetId !== null) {
      turnstile.remove(this.widgetId)
      this.widgetId = null
    }
  }

  getToken(): string | undefined {
    if (this.widgetId === null) return undefined
    return turnstile.getResponse(this.widgetId)
  }
}

// Usage
const manager = new TurnstileManager(SITE_KEY)
manager.render('#container', {
  onSuccess: (token) => console.log('Token:', token),
  onError: (error) => console.error('Error:', error),
})

适用场景:需要以编程方式控制小组件的 SPA


使用捆绑资源

脚本(scripts/)
  • check-csp.sh - 验证内容安全策略是否允许 Turnstile 脚本和 iframe

使用示例:

./scripts/check-csp.sh https://example.com
参考资料(references/)
  • references/widget-configs.md - 所有小组件配置选项的完整参考
  • references/error-codes.md - 全面的错误代码参考及故障排除说明
  • references/testing-guide.md - 测试策略、虚拟密钥、E2E 模式
  • references/react-integration.md - React 专属模式及 @marsidev/react-turnstile 用法

Claude 应在以下情况下加载这些资料:

  • widget-configs.md:配置小组件外观、主题或执行模式时
  • error-codes.md:排查 100*、200*、300*、400*、600* 错误代码时
  • testing-guide.md:设置 E2E 测试或本地开发时
  • react-integration.md:与 React、Next.js 集成或遇到 React 特有问题时
模板(templates/)
  • wrangler-turnstile-config.jsonc - Cloudflare Workers 环境配置
  • turnstile-widget-implicit.html - 隐式渲染 HTML 示例
  • turnstile-widget-explicit.ts - 显式渲染 JavaScript API
  • turnstile-server-validation.ts - Siteverify API 验证函数
  • turnstile-react-component.tsx - 使用 @marsidev/react-turnstile 的 React 组件
  • turnstile-hono-route.ts - 带验证功能的 Hono 路由处理程序
  • turnstile-test-config.ts - 使用虚拟密钥的测试配置

高级主题

SPA 的预先验证

Turnstile 可以签发预先验证 Cookie,使其在单页应用中跨页面导航持久有效。

turnstile.render('#container', {
  sitekey: SITE_KEY,
  callback: async (token) => {
    // Request pre-clearance cookie
    await fetch('/api/pre-clearance', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    })
  },
})
自定义操作和 cData

跟踪不同的质询类型或传递自定义数据:

turnstile.render('#container', {
  sitekey: SITE_KEY,
  action: 'login', // Track action in analytics
  cdata: JSON.stringify({ userId: '123' }), // Custom data (max 255 chars)
  callback: (token) => {
    // Token includes action and cdata for server validation
  },
})

服务端验证:

const result = await validateTurnstile(token, secretKey)

if (result.action !== 'login') {
  return new Response('Invalid action', { status: 400 })
}

const customData = JSON.parse(result.cdata || '{}')
重试和错误处理策略
class TurnstileWithRetry {
  private retryCount = 0
  private maxRetries = 3

  render(containerId: string) {
    turnstile.render(containerId, {
      sitekey: SITE_KEY,
      retry: 'auto', // or 'never' for manual control
      'retry-interval': 8000, // ms between retries
      'error-callback': (error) => {
        this.handleError(error)
      },
    })
  }

  private handleError(error: string) {
    // Error codes that should not retry
    const noRetry = ['110100', '110200', '110500']

    if (noRetry.some(code => error.includes(code))) {
      this.showFallback()
      return
    }

    // Retry on transient errors
    if (this.retryCount < this.maxRetries) {
      this.retryCount++
      setTimeout(() => {
        turnstile.reset(this.widgetId)
      }, 2000 * this.retryCount) // Exponential backoff
    } else {
      this.showFallback()
    }
  }

  private showFallback() {
    // Show alternative verification method
    console.error('Turnstile failed - showing fallback')
  }
}
多小组件页面
const widgets = {
  login: null as string | null,
  signup: null as string | null,
}

// Render multiple widgets
widgets.login = turnstile.render('#login-widget', {
  sitekey: SITE_KEY,
  action: 'login',
})

widgets.signup = turnstile.render('#signup-widget', {
  sitekey: SITE_KEY,
  action: 'signup',
})

// Reset specific widget
turnstile.reset(widgets.login)

// Get token from specific widget
const loginToken = turnstile.getResponse(widgets.login)

依赖项

必需:

  • 无(Turnstile 从 CDN 加载)

可选(React):

可选(其他框架):

  • vue-turnstile - Vue 3 集成
  • cfturnstile-vue3 - 另一种 Vue 3 封装
  • ngx-turnstile - Angular 集成
  • svelte-turnstile - Svelte 集成
  • @nuxtjs/turnstile - Nuxt 全栈集成

官方文档


软件包版本(已于 2025-10-22 验证)

{
  "devDependencies": {
    "@marsidev/react-turnstile": "^1.3.1",
    "turnstile-types": "^1.2.3"
  }
}

说明:

  • @marsidev/react-turnstile 是 Cloudflare 推荐的 React 软件包
  • 最近更新于 2025 年 9 月(积极维护中)
  • 兼容 React 18+、Next.js 13+、Next.js 14+、Next.js 15+

生产环境示例

此技能基于生产环境中的实现:

  • Cloudflare Workers:官方 HTMLRewriter 示例
  • React 应用:@marsidev/react-turnstile(经 Cloudflare 验证)
  • 社区:WordPress、Craft CMS、SilverStripe、Statamic 集成
  • 验证:✅ 已记录并预防所有 12 个已知问题

故障排除

问题:错误 110200 - “未知域名”

解决方案:在 Cloudflare Dashboard 中将你的域名(包括用于开发的 localhost)添加到组件的允许域名列表中。进行本地开发时,请改用虚拟测试 sitekey 1x00000000000000000000AA。

问题:错误 300030 - 组件导致合法用户遇到崩溃

解决方案:实现带重试逻辑的错误回调。这是 Cloudflare 端的已知问题(2025 年)。如果重试失败,请回退到其他验证方式。

问题:令牌始终返回 success: false

解决方案:

  1. 检查令牌是否已过期(TTL 为 5 分钟)
  2. 验证密钥是否正确
  3. 确保令牌尚未经过验证(只能使用一次)
  4. 检查主机名是否与组件配置匹配
问题:CSP 阻止 iframe(错误 200500)

解决方案:添加 CSP 指令:

<meta http-equiv="Content-Security-Policy" content="
  frame-src https://challenges.cloudflare.com;
  script-src https://challenges.cloudflare.com;
">
问题:Safari 18 的“隐藏 IP 地址”导致错误 300010

解决方案:在错误消息中说明用户应关闭 Safari 的“隐藏 IP 地址”设置(Safari → 设置 → 隐私 → 隐藏 IP 地址 → 关闭)

问题:Next.js + Jest 测试使用 @marsidev/react-turnstile 时失败

解决方案:在 Jest 设置中模拟 Turnstile 组件:

// jest.setup.ts
jest.mock('@marsidev/react-turnstile', () => ({
  Turnstile: () => <div data-testid="turnstile-mock" />,
}))

完整设置清单

使用此清单验证你的设置:

  • 已在 Cloudflare Dashboard 中创建 Turnstile 小组件
  • 已添加允许的域名(包括用于开发的 localhost)
  • 前端小组件从 https://challenges.cloudflare.com/turnstile/v0/api.js 加载
  • 小组件使用正确的 sitekey 渲染
  • 已实现并测试错误回调
  • 已实现服务器端 Siteverify 验证
  • 密钥存储在环境变量中(未硬编码)
  • 令牌验证包含 remoteip 检查
  • CSP 允许 challenges.cloudflare.com(如果使用 CSP)
  • 测试使用虚拟 sitekey(1x00000000000000000000AA)
  • 已实现令牌过期处理(TTL 为 5 分钟)
  • 已测试小组件的无障碍功能(键盘导航、屏幕阅读器)
  • 错误状态会显示对用户友好的消息
  • 生产部署使用与开发/预发布环境分开的组件

有问题?遇到故障?

  1. 查看 references/error-codes.md,了解特定错误的排查方法
  2. 核对 3 步设置流程中的所有步骤
  3. 查看官方文档:https://developers.cloudflare.com/turnstile/
  4. 确保已实现服务器端验证(最常见的问题)
  5. 使用 Cloudflare Docs MCP 工具:mcp__cloudflare-docs__search_cloudflare_documentation

令牌效率:节省约 65%–70%(10–12k 个令牌 → 3–4k 个令牌)

可避免的错误:12 个有完整解决方案的已记录问题

评论

支持 Markdown