关于 cloudflare-turnstile
cloudflare-turnstile 是 Cloudflare Turnstile 集成指南 Skill,涵盖前端组件和服务端令牌验证,并为 Web 开发者提供常见集成、测试、安全要求及故障排查信息。
主要功能
- 01配置隐式和显式组件
- 02在 React 中集成 Turnstile
- 03使用 Siteverify 在服务端验证令牌
- 04使用测试密钥和验证模式
- 05排查常见错误及 CSP 问题
适用场景
- 01为网站表单添加机器人验证
- 02在 API 路由中验证 Turnstile 令牌
- 03测试验证成功和失败场景
- 04诊断组件加载或验证问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill cloudflare-turnstile安装后在 Claude Code 中输入/cloudflare-turnstile确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/error-codes.mdexample-reference.mdreact-integration.mdtesting-guide.mdwidget-configs.mdrules/cloudflare-turnstile.mdscripts/check-csp.shexample-script.shtemplates/turnstile-hono-route.tsturnstile-react-component.tsxturnstile-server-validation.tsturnstile-test-config.tsturnstile-widget-explicit.tsturnstile-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
官方文档
- https://developers.cloudflare.com/turnstile/
- 使用
mcp__cloudflare-docs__search_cloudflare_documentation工具
故障排除
问题:错误 110200 - “未知域名”
解决方案:在 Cloudflare 控制面板中,将你的域名(包括用于开发的 localhost)添加到组件的允许域名列表中。对于本地开发,请改用虚拟测试站点密钥 1x00000000000000000000AA。
问题:错误 300030 - 合法用户的组件崩溃
解决方案:实现带重试逻辑的错误回调。这是 Cloudflare 端的已知问题(2025 年)。如果重试失败,请回退到其他验证方式。
问题:令牌始终返回 success: false
解决方案:
- 检查令牌是否已过期(TTL 为 5 分钟)
- 确认密钥正确
- 确保令牌尚未被验证过(只能使用一次)
- 检查主机名是否与组件配置匹配
问题: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、多个组件同时渲染、令牌重新生成模式)