mcpField
C
cloudflare-turnstile

cloudflare-turnstile

github.com/jezweb安全1.1k347 次安装3 周前更新
代码仓库

关于 cloudflare-turnstile

cloudflare-turnstile 是 Cloudflare Turnstile 集成指南 Skill,涵盖前端组件和服务端令牌验证,并为 Web 开发者提供常见集成、测试、安全要求及故障排查信息。

主要功能

  1. 01配置隐式和显式组件
  2. 02在 React 中集成 Turnstile
  3. 03使用 Siteverify 在服务端验证令牌
  4. 04使用测试密钥和验证模式
  5. 05排查常见错误及 CSP 问题

适用场景

  1. 01为网站表单添加机器人验证
  2. 02在 API 路由中验证 Turnstile 令牌
  3. 03测试验证成功和失败场景
  4. 04诊断组件加载或验证问题

安装

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

$npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-turnstile

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
error-codes.md
example-reference.md
react-integration.md
testing-guide.md
widget-configs.md
rules/
cloudflare-turnstile.md
scripts/
check-csp.sh
example-script.sh
templates/
turnstile-hono-route.ts
turnstile-react-component.tsx
turnstile-server-validation.ts
turnstile-test-config.ts
turnstile-widget-explicit.ts
turnstile-widget-implicit.html

详细介绍

Cloudflare Turnstile

状态:已可用于生产环境 ✅ 最后更新:2026-01-21 依赖项:无(可选:用于 React 的 @marsidev/react-turnstile) 最新版本:@marsidev/[email protected]、[email protected]

近期更新(2025):

  • 2025 年 12 月:@marsidev/react-turnstile v1.4.1 修复脚本加载中的竞态条件
  • 2025 年 8 月:v1.3.0 为 React 闭包问题新增 rerenderOnCallbackChange 属性
  • 2025 年 3 月:升级 Turnstile Analytics,增加 TopN 统计(7 个维度:主机名、浏览器、国家/地区、用户代理、ASN、操作系统、源 IP)、异常检测及增强的机器人行为监控
  • 2025 年 1 月:短暂强制执行 remoteip 验证(现已解决,但凸显了正确传递 IP 的重要性)
  • 2025 年:符合 WCAG 2.1 AA 标准;免费计划(20 个小组件、7 天分析数据);企业功能(不限数量的小组件、临时 ID、支持任意主机名、30 天分析数据、offlabel 品牌标识)

快速开始(5 分钟)

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

# 2. Add widget to frontend
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form>
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Submit</button>
</form>

# 3. Validate token server-side (Cloudflare Workers)
const formData = await request.formData()
const token = formData.get('cf-turnstile-response')

const verifyFormData = new FormData()
verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token)
verifyFormData.append('remoteip', request.headers.get('CF-Connecting-IP'))  // REQUIRED - see Critical Rules

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', { status: 401 })

重要:

  • 令牌 5 分钟后过期,且只能使用一次
  • 始终在服务端验证(必须调用 Siteverify API)
  • 切勿代理或缓存 api.js(必须从 Cloudflare CDN 加载)
  • 为开发、预发布和生产环境使用不同的小组件

渲染模式

隐式(页面加载时自动渲染):

<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"></div>

显式(适用于 SPA 的编程式控制):

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' })
turnstile.reset(widgetId)   // Reset widget
turnstile.getResponse(widgetId)  // Get token

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

import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />

关键规则

务必执行

✅ 调用 Siteverify API — 必须在服务端验证 ✅ 使用 HTTPS — 切勿通过 HTTP 验证 ✅ 保护密钥 — 切勿在前端代码中暴露 ✅ 处理令牌过期 — 令牌 5 分钟后过期 ✅ 实现错误回调 — 妥善处理失败 ✅ 使用测试用虚拟密钥 — 测试 sitekey:1x00000000000000000000AA ✅ 设置合理的超时时间 — 不要无限期等待验证 ✅ 验证 action/hostname — 如果指定了其他字段,也要检查 ✅ 定期轮换密钥 — 使用控制面板或 API 轮换密钥 ✅ 监控分析数据 — 跟踪通过率和失败情况 ✅ 始终向 Siteverify 传递客户端 IP — 使用 CF-Connecting-IP 标头(Workers)或 X-Forwarded-For(Node.js)。Cloudflare 曾于 2025 年 1 月短暂强制执行严格的 remoteip 验证,导致未传递正确 IP 的网站普遍出现故障

切勿执行

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


已知问题预防

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

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

错误:Turnstile Analytics 控制面板中没有令牌验证记录 来源: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 秒后过期 预防措施:模板会说明 TTL,并在令牌过期时刷新令牌

问题 #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——“加载错误:无法加载 iframe” 来源: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(2025 年 4 月) 原因:Brave 防护功能阻止了动画脚本 预防:模板会在动画完成前处理成功状态

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

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

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

错误:错误 110200——“未知域名:不允许此域名” 来源:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因:开发环境中使用了生产组件,但允许列表中没有 localhost 预防:模板在开发环境中使用虚拟测试密钥,并说明 localhost 允许列表要求

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

错误:返回 success: false,并显示“令牌已使用”错误 来源:https://developers.cloudflare.com/turnstile/troubleshooting/testing 原因:每个令牌只能验证一次。Turnstile 令牌只能单次使用——验证后(无论成功还是失败),令牌都会被消耗,无法再次验证。开发者必须显式调用 turnstile.reset(),为后续提交生成新令牌。 预防:模板说明单次使用限制和令牌刷新模式

// CRITICAL: Reset widget after validation to get new token
const turnstileRef = useRef(null)

async function handleSubmit(e) {
  e.preventDefault()
  const token = formData.get('cf-turnstile-response')

  const result = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify({ token })
  })

  // Reset widget regardless of success/failure
  // Token is consumed either way
  if (turnstileRef.current) {
    turnstile.reset(turnstileRef.current)
  }
}

<Turnstile
  ref={turnstileRef}
  siteKey={TURNSTILE_SITE_KEY}
  onSuccess={setToken}
/>
问题 #13:错误 106010 - Chrome/Edge 首次加载失败

错误:Chrome/Edge 浏览器首次加载小组件时出现 106010——“通用参数错误” 来源:Cloudflare 错误代码,社区报告 原因:未知的浏览器特定问题,影响 Chrome 和 Edge 首次加载页面时的情况。控制台显示对 https://challenges.cloudflare.com/cdn-cgi/challenge-platform 的请求返回 400 错误。Firefox 不受影响。后续重新加载页面后即可正常工作。 预防方法:实现带自动重试逻辑的错误回调

turnstile.render('#container', {
  sitekey: SITE_KEY,
  retry: 'auto',
  'retry-interval': 8000,
  'error-callback': (errorCode) => {
    if (errorCode === '106010') {
      console.warn('Chrome/Edge first-load issue (106010), auto-retrying...')
      // Auto-retry will handle it
    }
  }
})

解决方法:重新加载页面后,小组件即可正常工作。启用自动重试设置可解决大多数情况。请在无痕模式下测试,以排除浏览器扩展的影响。检查 CSP 规则,确保允许 Cloudflare Turnstile 端点。

问题 #14:多个小组件的可视状态卡住(来自社区)

错误:即使成功生成令牌,小组件仍显示“Pending...”状态 来源:GitHub 问题 #119 原因:在单个页面上渲染多个 <Turnstile/> 组件时出现 CSS 重绘问题。仅能在全高清桌面屏幕上复现。令牌确实已成功生成(验证正常),但可视状态不会更新。将鼠标悬停在小组件上会触发重绘,并显示正确状态。 预防方法:在成功回调中强制进行 CSS 重绘

<Turnstile
  siteKey={KEY}
  onSuccess={(token) => {
    setToken(token)
    // Force repaint by toggling display
    const widget = document.querySelector('.cf-turnstile')
    if (widget) {
      widget.style.display = 'none'
      setTimeout(() => widget.style.display = 'block', 0)
    }
  }}
/>

注意:这只是视觉问题,并非验证失败。令牌已正确生成且可以正常使用。

问题 #15:Jest 与 @marsidev/react-turnstile 的兼容性(2025 年 12 月更新)

错误:导入 @marsidev/react-turnstile 时出现 Jest encountered an unexpected token 来源:GitHub 问题 #114,GitHub 问题 #112 原因:Jest 30.2.0(截至 2025 年 12 月的最新版本)存在 ESM 模块解析问题。维护者将问题 #112 以“计划外”关闭。Jest 用户目前无法解决此问题;迁移到 Vitest 后即可正常工作。 预防方法:在 Jest 设置中模拟 Turnstile 组件,或迁移到 Vitest

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

// Option 2: transformIgnorePatterns in jest.config.js
module.exports = {
  transformIgnorePatterns: [
    'node_modules/(?!(@marsidev/react-turnstile)/)'
  ]
}

// Option 3 (Recommended): Migrate to Vitest
// Vitest handles ESM modules correctly without mocking

状态:维护者已将问题以“计划外”关闭。建议新项目迁移到 Vitest。

配置

wrangler.jsonc:

{
  "vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
  "secrets": ["TURNSTILE_SECRET_KEY"]  // Run: wrangler secret put TURNSTILE_SECRET_KEY
}

必需的 CSP:

<meta http-equiv="Content-Security-Policy" content="
  script-src 'self' https://challenges.cloudflare.com;
  frame-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') || '')  // CRITICAL - always pass client 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 的客户端表单


测试密钥

虚拟站点密钥(客户端):

  • 始终通过:1x00000000000000000000AA
  • 始终拦截:2x00000000000000000000AB
  • 强制交互:3x00000000000000000000FF

虚拟密钥(服务器):

  • 始终通过:1x0000000000000000000000000000000AA
  • 始终失败:2x0000000000000000000000000000000AA
  • 令牌已使用:3x0000000000000000000000000000000AA

随附资源

脚本: check-csp.sh - 验证 CSP 是否允许 Turnstile

参考资料:

  • widget-configs.md - 所有配置选项
  • error-codes.md - 错误代码排查指南(100*/200*/300*/400*/600*)
  • testing-guide.md - 测试策略、虚拟密钥
  • react-integration.md - React/Next.js 模式

模板: Hono、React、隐式/显式渲染和验证的完整示例


高级功能

预通行(SPA): 签发可在页面导航间持续有效的 Cookie

turnstile.render('#container', {
  sitekey: SITE_KEY,
  callback: async (token) => {
    await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
  }
})

自定义操作和数据: 跟踪挑战类型,传递自定义数据(最多 255 个字符)

turnstile.render('#container', {
  action: 'login',  // Track in analytics
  cdata: JSON.stringify({ userId: '123' }),  // Custom payload
})

错误处理: 使用 retry: 'auto' 和 error-callback 增强弹性

turnstile.render('#container', {
  retry: 'auto',
  'retry-interval': 8000,  // ms between retries
  'error-callback': (error) => { /* handle or show fallback */ }
})

依赖项

必需: 无(从 CDN 加载) React: @marsidev/[email protected](Cloudflare 推荐),[email protected] 其他: vue-turnstile、ngx-turnstile、svelte-turnstile、@nuxtjs/turnstile


官方文档


故障排除

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

解决方案:在 Cloudflare 控制面板中,将你的域名(包括用于开发的 localhost)添加到组件的允许域名列表中。对于本地开发,请改用虚拟测试站点密钥 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 组件(或迁移到 Vitest,它能正确处理 ESM 模块):

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

// Option 2: Migrate to Vitest (recommended for new projects)
// Vitest handles ESM modules without mocking required

注意:此问题在 Jest 30.2.0(2025 年 12 月)中仍然存在。维护者以“暂不计划处理”为由关闭了该问题。有关详情,请参阅 Issue #15。


可预防的错误:已记录 15 个问题(Safari 18 隐藏 IP、Brave 彩纸效果、Next.js Jest、CSP 阻止、令牌重复使用、过期、主机名允许列表、组件崩溃 300030、配置错误 600010、缺少验证、GET 请求、密钥泄露、Chrome/Edge 106010、多个组件同时渲染、令牌重新生成模式)

评论

支持 Markdown