cloudflare-turnstile
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCloudflare Turnstile
Cloudflare Turnstile
Status: Production Ready ✅
Last Updated: 2026-01-21
Dependencies: None (optional: @marsidev/react-turnstile for React)
Latest Versions: @marsidev/react-turnstile@1.4.1, turnstile-types@1.2.3
Recent Updates (2025):
- December 2025: @marsidev/react-turnstile v1.4.1 fixes race condition in script loading
- August 2025: v1.3.0 adds prop for React closure issues
rerenderOnCallbackChange - March 2025: Upgraded Turnstile Analytics with TopN statistics (7 dimensions: hostnames, browsers, countries, user agents, ASNs, OS, source IPs), anomaly detection, enhanced bot behavior monitoring
- January 2025: Brief remoteip validation enforcement (resolved, but highlights importance of correct IP passing)
- 2025: WCAG 2.1 AA compliance, Free plan (20 widgets, 7-day analytics), Enterprise features (unlimited widgets, ephemeral IDs, any hostname support, 30-day analytics, offlabel branding)
状态: 已就绪可用于生产环境 ✅
最后更新: 2026-01-21
依赖: 无(可选:React项目可使用@marsidev/react-turnstile)
最新版本: @marsidev/react-turnstile@1.4.1, turnstile-types@1.2.3
2025年近期更新:
- 2025年12月: @marsidev/react-turnstile v1.4.1修复了脚本加载中的竞态条件
- 2025年8月: v1.3.0新增属性,解决React闭包问题
rerenderOnCallbackChange - 2025年3月: 升级Turnstile分析功能,新增TopN统计(7个维度:主机名、浏览器、国家、用户代理、ASN、操作系统、源IP)、异常检测、增强型机器人行为监控
- 2025年1月: 短暂强制执行remoteip验证(已解决,但凸显了传递正确IP的重要性)
- 2025年: 符合WCAG 2.1 AA合规标准,免费计划(20个小部件、7天分析数据),企业版功能(无限小部件、临时ID、支持任意主机名、30天分析数据、自定义品牌)
Quick Start (5 Minutes)
快速入门(5分钟)
bash
undefinedbash
undefined1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile
Copy sitekey (public) and secret key (private)
复制站点密钥(公开)和密钥(私有)
2. Add widget to frontend
2. 将小部件添加到前端
<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>
<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)
3. 服务器端验证令牌(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 })
**CRITICAL:**
- Token expires in 5 minutes, single-use only
- ALWAYS validate server-side (Siteverify API required)
- Never proxy/cache api.js (must load from Cloudflare CDN)
- Use different widgets for dev/staging/productionconst 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加载)
- 开发/预发布/生产环境使用不同的小部件Rendering Modes
渲染模式
Implicit (auto-render on page load):
html
<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>Explicit (programmatic control for SPAs):
typescript
<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 tokenReact (using @marsidev/react-turnstile):
tsx
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />隐式渲染(页面加载时自动渲染):
html
<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>显式渲染(单页应用的程序化控制):
typescript
<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 tokenReact集成(使用@marsidev/react-turnstile):
tsx
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />Critical Rules
关键规则
Always Do
必须执行的操作
✅ Call Siteverify API - Server-side validation is mandatory
✅ Use HTTPS - Never validate over HTTP
✅ Protect secret keys - Never expose in frontend code
✅ Handle token expiration - Tokens expire after 5 minutes
✅ Implement error callbacks - Handle failures gracefully
✅ Use dummy keys for testing - Test sitekey:
✅ Set reasonable timeouts - Don't wait indefinitely for validation
✅ Validate action/hostname - Check additional fields when specified
✅ Rotate keys periodically - Use dashboard or API to rotate secrets
✅ Monitor analytics - Track solve rates and failures
✅ Always pass client IP to Siteverify - Use header (Workers) or (Node.js). Cloudflare briefly enforced strict remoteip validation in Jan 2025, causing widespread failures for sites not passing correct IP
1x00000000000000000000AACF-Connecting-IPX-Forwarded-For✅ 调用Siteverify API - 服务器端验证是强制要求
✅ 使用HTTPS - 切勿通过HTTP进行验证
✅ 保护密钥 - 切勿在前端代码中暴露
✅ 处理令牌过期 - 令牌5分钟后过期
✅ 实现错误回调 - 优雅处理失败情况
✅ 测试使用虚拟密钥 - 测试站点密钥:
✅ 设置合理超时 - 不要无限等待验证结果
✅ 验证操作/主机名 - 指定时检查额外字段
✅ 定期轮换密钥 - 使用控制台或API轮换密钥
✅ 监控分析数据 - 跟踪解决率和失败情况
✅ 始终将客户端IP传递给Siteverify - 使用头(Workers)或(Node.js)。Cloudflare曾在2025年1月短暂强制执行严格的remoteip验证,导致未传递正确IP的站点出现大量失败
1x00000000000000000000AACF-Connecting-IPX-Forwarded-ForNever Do
切勿执行的操作
❌ Skip server validation - Client-side only = security vulnerability
❌ Proxy api.js script - Must load from Cloudflare CDN
❌ Reuse tokens - Each token is single-use only
❌ Use GET requests - Siteverify only accepts POST
❌ Expose secret key - Keep secrets in backend environment only
❌ Trust client-side validation - Tokens can be forged
❌ Cache api.js - Future updates will break your integration
❌ Use production keys in tests - Use dummy keys instead
❌ Ignore error callbacks - Always handle failures
❌ 跳过服务器端验证 - 仅客户端验证会导致安全漏洞
❌ 代理api.js脚本 - 必须从Cloudflare CDN加载
❌ 重复使用令牌 - 每个令牌仅限单次使用
❌ 使用GET请求 - Siteverify仅接受POST请求
❌ 暴露密钥 - 密钥仅保留在后端环境中
❌ 信任客户端验证 - 令牌可能被伪造
❌ 缓存api.js - 未来的更新会破坏你的集成
❌ 测试中使用生产密钥 - 改用虚拟密钥
❌ 忽略错误回调 - 始终处理失败情况
Known Issues Prevention
已知问题预防
This skill prevents 15 documented issues:
本技能可预防15个已记录的问题:
Issue #1: Missing Server-Side Validation
问题#1:缺少服务器端验证
Error: Zero token validation in Turnstile Analytics dashboard
Source: https://developers.cloudflare.com/turnstile/get-started/
Why It Happens: Developers only implement client-side widget, skip Siteverify call
Prevention: All templates include mandatory server-side validation with Siteverify API
错误: Turnstile分析仪表板中显示令牌验证为零
来源: https://developers.cloudflare.com/turnstile/get-started/
原因: 开发者仅实现了客户端小部件,跳过了Siteverify调用
预防: 所有模板都包含使用Siteverify API的强制服务器端验证
Issue #2: Token Expiration (5 Minutes)
问题#2:令牌过期(5分钟)
Error: for valid tokens submitted after delay
Source: https://developers.cloudflare.com/turnstile/get-started/server-side-validation
Why It Happens: Tokens expire 300 seconds after generation
Prevention: Templates document TTL and implement token refresh on expiration
success: false错误: 延迟提交的有效令牌返回
来源: https://developers.cloudflare.com/turnstile/get-started/server-side-validation
原因: 令牌生成300秒后过期
预防: 模板中记录了TTL,并实现了令牌过期时的刷新逻辑
success: falseIssue #3: Secret Key Exposed in Frontend
问题#3:密钥在前端暴露
Error: Security bypass - attackers can validate their own tokens
Source: https://developers.cloudflare.com/turnstile/get-started/server-side-validation
Why It Happens: Secret key hardcoded in JavaScript or visible in source
Prevention: All templates show backend-only validation with environment variables
错误: 安全绕过 - 攻击者可自行验证令牌
来源: https://developers.cloudflare.com/turnstile/get-started/server-side-validation
原因: 密钥硬编码在JavaScript中或在源码中可见
预防: 所有模板展示了仅在后端使用环境变量的验证方式
Issue #4: GET Request to Siteverify
问题#4:对Siteverify使用GET请求
Error: API returns 405 Method Not Allowed
Source: https://developers.cloudflare.com/turnstile/migration/recaptcha
Why It Happens: reCAPTCHA supports GET, Turnstile requires POST
Prevention: Templates use POST with FormData or JSON body
错误: API返回405 Method Not Allowed
来源: https://developers.cloudflare.com/turnstile/migration/recaptcha
原因: reCAPTCHA支持GET,而Turnstile要求POST
预防: 模板使用POST请求,搭配FormData或JSON主体
Issue #5: Content Security Policy Blocking
问题#5:内容安全策略阻止加载
Error: Error 200500 - "Loading error: The iframe could not be loaded"
Source: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes
Why It Happens: CSP blocks challenges.cloudflare.com iframe
Prevention: Skill includes CSP configuration reference and check-csp.sh script
错误: 错误200500 - "加载错误:无法加载iframe"
来源: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes
原因: CSP阻止了challenges.cloudflare.com的iframe
预防: 本技能包含CSP配置参考和check-csp.sh脚本
Issue #6: Widget Crash (Error 300030)
问题#6:小部件崩溃(错误300030)
Error: Generic client execution error for legitimate users
Source: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903
Why It Happens: Unknown - appears to be Cloudflare-side issue (2025)
Prevention: Templates implement error callbacks, retry logic, and fallback handling
错误: 合法用户遇到通用客户端执行错误
来源: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903
原因: 未知 - 似乎是Cloudflare端的问题(2025年)
预防: 模板实现了错误回调、重试逻辑和降级处理
Issue #7: Configuration Error (Error 600010)
问题#7:配置错误(错误600010)
Error: Widget fails with "configuration error"
Source: https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578
Why It Happens: Missing or deleted hostname in widget configuration
Prevention: Templates document hostname allowlist requirement and verification steps
错误: 小部件因"配置错误"失败
来源: https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578
原因: 小部件配置中缺少或删除了主机名
预防: 模板记录了主机名白名单要求和验证步骤
Issue #8: Safari 18 / macOS 15 "Hide IP" Issue
问题#8:Safari 18 / macOS 15 "隐藏IP"问题
Error: Error 300010 when Safari's "Hide IP address" is enabled
Source: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903
Why It Happens: Privacy settings interfere with challenge signals
Prevention: Error handling reference documents Safari workaround (disable Hide IP)
错误: 启用Safari的"隐藏IP地址"时出现错误300010
来源: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903
原因: 隐私设置干扰了挑战信号
预防: 错误处理参考文档中记录了Safari的解决方法(禁用隐藏IP)
Issue #9: Brave Browser Confetti Animation Failure
问题#9:Brave浏览器庆祝动画失败
Error: Verification fails during success animation
Source: https://github.com/brave/brave-browser/issues/45608 (April 2025)
Why It Happens: Brave shields block animation scripts
Prevention: Templates handle success before animation completes
错误: 验证在成功动画期间失败
来源: https://github.com/brave/brave-browser/issues/45608 (April 2025)
原因: Brave护盾阻止了动画脚本
预防: 模板在动画完成前处理成功逻辑
Issue #10: Next.js + Jest Incompatibility
问题#10:Next.js + Jest不兼容
Error: @marsidev/react-turnstile breaks Jest tests
Source: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025)
Why It Happens: Module resolution issues with Jest
Prevention: Testing guide includes Jest mocking patterns and dummy sitekey usage
错误: @marsidev/react-turnstile导致Jest测试失败
来源: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025)
原因: Jest的模块解析问题
预防: 测试指南包含Jest模拟模式和虚拟站点密钥的使用方法
Issue #11: localhost Not in Allowlist
问题#11:localhost不在白名单中
Error: Error 110200 - "Unknown domain: Domain not allowed"
Source: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes
Why It Happens: Production widget used in development without localhost in allowlist
Prevention: Templates use dummy test keys for dev, document localhost allowlist requirement
错误: 错误110200 - "未知域名:域名不被允许"
来源: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes
原因: 开发环境中使用了生产小部件,且未将localhost加入白名单
预防: 模板在开发环境使用虚拟测试密钥,并记录了localhost白名单要求
Issue #12: Token Reuse Attempt
问题#12:尝试重复使用令牌
Error: with "token already spent" error
Source: https://developers.cloudflare.com/turnstile/troubleshooting/testing
Why It Happens: Each token can only be validated once. Turnstile tokens are single-use - after validation (success OR failure), the token is consumed and cannot be revalidated. Developers must explicitly call to generate a new token for subsequent submissions.
Prevention: Templates document single-use constraint and token refresh patterns
success: falseturnstile.reset()typescript
// 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}
/>错误: 并提示"token already spent"
来源: https://developers.cloudflare.com/turnstile/troubleshooting/testing
原因: 每个令牌只能验证一次。Turnstile令牌为单次使用 - 验证后(成功或失败),令牌即被消耗,无法重新验证。开发者必须显式调用来生成新令牌用于后续提交。
预防: 模板记录了单次使用限制和令牌刷新模式
success: falseturnstile.reset()typescript
// 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}
/>Issue #13: Error 106010 - Chrome/Edge First-Load Failure
问题#13:错误106010 - Chrome/Edge首次加载失败
Error: - "Generic parameter error" on first widget load in Chrome/Edge browsers
Source: Cloudflare Error Codes, Community Report
Why It Happens: Unknown browser-specific issue affecting Chrome and Edge on first page load. Console shows 400 error to . Firefox is not affected. Subsequent page reloads work correctly.
Prevention: Implement error callback with auto-retry logic
106010https://challenges.cloudflare.com/cdn-cgi/challenge-platformtypescript
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
}
}
})Workaround: Widget works correctly after page reload. Auto-retry setting resolves in most cases. Test in Incognito mode to rule out browser extensions. Review CSP rules to ensure Cloudflare Turnstile endpoints are allowed.
错误: - Chrome/Edge浏览器首次加载小部件时出现"通用参数错误"
来源: Cloudflare错误代码, 社区报告
原因: 未知的浏览器特定问题,影响Chrome和Edge的首次页面加载。控制台显示对的400错误。Firefox不受影响。后续页面刷新可正常工作。
预防: 实现带自动重试逻辑的错误回调
106010https://challenges.cloudflare.com/cdn-cgi/challenge-platformtypescript
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端点。
Issue #14: Multiple Widgets Visual Status Stuck (Community-sourced)
问题#14:多小部件视觉状态卡住(社区反馈)
Error: Widget displays "Pending..." status even after successful token generation
Source: GitHub Issue #119
Why It Happens: CSS repaint issue when rendering multiple components on a single page. Only reproducible on full HD desktop screens. Token IS successfully generated (validation works), but visual status doesn't update. Hovering over widget triggers repaint and shows correct status.
Prevention: Force CSS repaint in success callback
<Turnstile/>tsx
<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)
}
}}
/>Note: This is a visual-only issue, not a validation failure. The token is correctly generated and functional.
错误: 即使令牌生成成功,小部件仍显示"Pending..."状态
来源: GitHub Issue #119
原因: 在单页渲染多个组件时出现CSS重绘问题。仅在全高清桌面屏幕上可复现。令牌已成功生成(验证有效),但视觉状态未更新。悬停在小部件上会触发重绘并显示正确状态。
预防: 在成功回调中强制CSS重绘
<Turnstile/>tsx
<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)
}
}}
/>注意: 这只是视觉问题,不影响验证功能。令牌已正确生成且可用。
Issue #15: Jest Compatibility with @marsidev/react-turnstile (Updated Dec 2025)
问题#15:Jest与@marsidev/react-turnstile兼容问题(2025年12月更新)
Error: when importing @marsidev/react-turnstile
Source: GitHub Issue #114, GitHub Issue #112
Why It Happens: ESM module resolution issues with Jest 30.2.0 (latest as of Dec 2025). Issue #112 closed as "not planned" by maintainer. Jest users are stuck; Vitest migration works.
Prevention: Mock the Turnstile component in Jest setup OR migrate to Vitest
Jest encountered an unexpected tokentypescript
// 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 mockingStatus: Maintainer closed issue as "not planned". Recommend migrating to Vitest for new projects.
错误: 导入@marsidev/react-turnstile时Jest遇到意外令牌
来源: GitHub Issue #114, GitHub Issue #112
原因: Jest 30.2.0(截至2025年12月为最新版本)的ESM模块解析问题。Issue #112被维护者标记为"不计划修复"。Jest用户陷入困境;迁移到Vitest可解决。
预防: 在Jest设置中模拟Turnstile组件,或迁移到Vitest
typescript
// 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。
Configuration
配置
wrangler.jsonc:
jsonc
{
"vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
"secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY
}Required CSP:
html
<meta http-equiv="Content-Security-Policy" content="
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
">wrangler.jsonc:
jsonc
{
"vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
"secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY
}必需的CSP:
html
<meta http-equiv="Content-Security-Policy" content="
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
">Common Patterns
常见模式
Pattern 1: Hono + Cloudflare Workers
模式1:Hono + Cloudflare Workers
typescript
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 appWhen to use: API routes in Cloudflare Workers with Hono framework
typescript
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路由
Pattern 2: React + Next.js App Router
模式2:React + Next.js App Router
tsx
'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>
)
}When to use: Client-side forms in Next.js with React hooks
tsx
'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>
)
}适用场景: 使用React Hooks的Next.js客户端表单
Testing Keys
测试密钥
Dummy Sitekeys (client):
- Always pass:
1x00000000000000000000AA - Always block:
2x00000000000000000000AB - Force interactive:
3x00000000000000000000FF
Dummy Secret Keys (server):
- Always pass:
1x0000000000000000000000000000000AA - Always fail:
2x0000000000000000000000000000000AA - Token already spent:
3x0000000000000000000000000000000AA
虚拟站点密钥(客户端):
- 始终通过:
1x00000000000000000000AA - 始终阻止:
2x00000000000000000000AB - 强制交互式:
3x00000000000000000000FF
虚拟密钥(服务器端):
- 始终通过:
1x0000000000000000000000000000000AA - 始终失败:
2x0000000000000000000000000000000AA - 令牌已消耗:
3x0000000000000000000000000000000AA
Bundled Resources
捆绑资源
Scripts: - Verify CSP allows Turnstile
check-csp.shReferences:
- - All configuration options
widget-configs.md - - Error code troubleshooting (100*/200*/300*/400*/600*)
error-codes.md - - Testing strategies, dummy keys
testing-guide.md - - React/Next.js patterns
react-integration.md
Templates: Complete examples for Hono, React, implicit/explicit rendering, validation
脚本: - 验证CSP是否允许Turnstile
check-csp.sh参考文档:
- - 所有配置选项
widget-configs.md - - 错误代码故障排除(100*/200*/300*/400*/600*)
error-codes.md - - 测试策略、虚拟密钥
testing-guide.md - - React/Next.js模式
react-integration.md
模板: Hono、React、隐式/显式渲染、验证的完整示例
Advanced Features
高级功能
Pre-Clearance (SPAs): Issue cookie that persists across page navigations
typescript
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
}
})Custom Actions & Data: Track challenge types, pass custom data (max 255 chars)
typescript
turnstile.render('#container', {
action: 'login', // Track in analytics
cdata: JSON.stringify({ userId: '123' }), // Custom payload
})Error Handling: Use and for resilience
retry: 'auto'error-callbacktypescript
turnstile.render('#container', {
retry: 'auto',
'retry-interval': 8000, // ms between retries
'error-callback': (error) => { /* handle or show fallback */ }
})预验证(单页应用): 颁发跨页面导航持久化的Cookie
typescript
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
}
})自定义操作与数据: 跟踪挑战类型,传递自定义数据(最多255字符)
typescript
turnstile.render('#container', {
action: 'login', // Track in analytics
cdata: JSON.stringify({ userId: '123' }), // Custom payload
})错误处理: 使用和提高韧性
retry: 'auto'error-callbacktypescript
turnstile.render('#container', {
retry: 'auto',
'retry-interval': 8000, // ms between retries
'error-callback': (error) => { /* handle or show fallback */ }
})Dependencies
依赖
Required: None (loads from CDN)
React: @marsidev/react-turnstile@1.4.1 (Cloudflare-recommended), turnstile-types@1.2.3
Other: vue-turnstile, ngx-turnstile, svelte-turnstile, @nuxtjs/turnstile
必需: 无(从CDN加载)
React: @marsidev/react-turnstile@1.4.1(Cloudflare推荐), turnstile-types@1.2.3
其他: vue-turnstile, ngx-turnstile, svelte-turnstile, @nuxtjs/turnstile
Official Documentation
官方文档
- https://developers.cloudflare.com/turnstile/
- Use tool
mcp__cloudflare-docs__search_cloudflare_documentation
- https://developers.cloudflare.com/turnstile/
- 使用工具
mcp__cloudflare-docs__search_cloudflare_documentation
Troubleshooting
故障排除
Problem: Error 110200 - "Unknown domain"
问题:错误110200 - "未知域名"
Solution: Add your domain (including localhost for dev) to widget's allowed domains in Cloudflare Dashboard. For local dev, use dummy test sitekey instead.
1x00000000000000000000AA解决方案: 将你的域名(包括开发环境的localhost)添加到Cloudflare控制台中小部件的允许域名列表。本地开发时,使用虚拟测试站点密钥替代。
1x00000000000000000000AAProblem: Error 300030 - Widget crashes for legitimate users
问题:错误300030 - 小部件为合法用户崩溃
Solution: Implement error callback with retry logic. This is a known Cloudflare-side issue (2025). Fallback to alternative verification if retries fail.
解决方案: 实现带重试逻辑的错误回调。这是Cloudflare端的已知问题(2025年)。如果重试失败,降级到其他验证方式。
Problem: Tokens always return success: false
success: false问题:令牌始终返回success: false
success: falseSolution:
- Check token hasn't expired (5 min TTL)
- Verify secret key is correct
- Ensure token hasn't been validated before (single-use)
- Check hostname matches widget configuration
解决方案:
- 检查令牌是否已过期(5分钟TTL)
- 验证密钥是否正确
- 确保令牌未被验证过(单次使用)
- 检查主机名是否与小部件配置匹配
Problem: CSP blocking iframe (Error 200500)
问题:CSP阻止iframe(错误200500)
Solution: Add CSP directives:
html
<meta http-equiv="Content-Security-Policy" content="
frame-src https://challenges.cloudflare.com;
script-src https://challenges.cloudflare.com;
">解决方案: 添加CSP指令:
html
<meta http-equiv="Content-Security-Policy" content="
frame-src https://challenges.cloudflare.com;
script-src https://challenges.cloudflare.com;
">Problem: Safari 18 "Hide IP" causing Error 300010
问题:Safari 18 "隐藏IP"导致错误300010
Solution: Document in error message that users should disable Safari's "Hide IP address" setting (Safari → Settings → Privacy → Hide IP address → Off)
解决方案: 在错误提示中告知用户应禁用Safari的"隐藏IP地址"设置(Safari → 设置 → 隐私 → 隐藏IP地址 → 关闭)
Problem: Next.js + Jest tests failing with @marsidev/react-turnstile
问题:Next.js + Jest测试因@marsidev/react-turnstile失败
Solution: Mock the Turnstile component in Jest setup (or migrate to Vitest, which handles ESM modules correctly):
typescript
// 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 requiredNote: This issue persists in Jest 30.2.0 (Dec 2025). Maintainer closed as "not planned". See Issue #15 for full details.
Errors Prevented: 15 documented issues (Safari 18 Hide IP, Brave confetti, Next.js Jest, CSP blocking, token reuse, expiration, hostname allowlist, widget crash 300030, config error 600010, missing validation, GET request, secret exposure, Chrome/Edge 106010, multiple widgets rendering, token regeneration pattern)
解决方案: 在Jest设置中模拟Turnstile组件(或迁移到Vitest,它能正确处理ESM模块):
typescript
// 选项1:Jest模拟(jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// 选项2:在jest.config.js中设置transformIgnorePatterns
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(@marsidev/react-turnstile)/)'
]
}
// 选项3(推荐):迁移到Vitest
// Vitest无需模拟即可正确处理ESM模块注意: 此问题在Jest 30.2.0(2025年12月)中仍存在。维护者已标记为"不计划修复"。详情见问题#15。
已预防的错误: 15个已记录的问题(Safari 18隐藏IP、Brave庆祝动画、Next.js Jest、CSP阻止、令牌重复使用、过期、主机名白名单、小部件崩溃300030、配置错误600010、缺少验证、GET请求、密钥暴露、Chrome/Edge 106010、多小部件渲染、令牌重新生成模式)