cloudflare-turnstile
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCloudflare Turnstile
Cloudflare Turnstile
Status: Production Ready
Last Updated: 2025-10-22
Dependencies: None (optional: @marsidev/react-turnstile for React)
Latest Versions: @marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3
状态:可用于生产环境
最后更新:2025-10-22
依赖:无(可选:React项目使用@marsidev/react-turnstile)
最新版本:@marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3
Quick Start (10 Minutes)
快速入门(10分钟)
1. Create Turnstile Widget
1. 创建Turnstile小部件
Get your sitekey and secret key from Cloudflare Dashboard.
bash
undefined从Cloudflare控制台获取你的sitekey(公钥)和secret key(私钥)。
bash
undefinedNavigate to: https://dash.cloudflare.com/?to=/:account/turnstile
Create new widget → Copy sitekey (public) and secret key (private)
创建新小部件 → 复制sitekey(公开)和secret key(私有)
**Why this matters:**
- Each widget has unique sitekey/secret pair
- Sitekey goes in frontend (public)
- Secret key ONLY in backend (private)
- Use different widgets for dev/staging/production
**重要性说明**:
- 每个小部件都有唯一的sitekey/secret密钥对
- Sitekey用于前端(公开)
- Secret key仅用于后端(私有)
- 开发/预发布/生产环境使用不同的小部件2. Add Widget to Frontend
2. 在前端添加小部件
Embed the Turnstile widget in your HTML form.
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>
<!-- Turnstile widget renders here -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
</body>
</html>CRITICAL:
- Never proxy or cache - must load from Cloudflare CDN
api.js - Widget auto-creates hidden input with token
cf-turnstile-response - Token expires in 5 minutes
- Each token is single-use only
在HTML表单中嵌入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>
<!-- Turnstile小部件将在此处渲染 -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">提交</button>
</form>
</body>
</html>关键注意事项:
- 切勿代理或缓存- 必须从Cloudflare CDN加载
api.js - 小部件会自动创建隐藏输入框,存储验证令牌
cf-turnstile-response - 令牌5分钟后过期
- 每个令牌仅可使用一次
3. Validate Token on Server
3. 在服务器端验证令牌
ALWAYS validate the token server-side. Client-side verification alone is not secure.
typescript
// 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!')
}
}必须在服务器端验证令牌,仅客户端验证并不安全。
typescript
// Cloudflare Workers示例
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')
// 通过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('无效的Turnstile令牌', { status: 401 })
}
// 令牌验证通过 - 继续处理表单
return new Response('提交成功!')
}
}The 3-Step Setup Process
三步设置流程
Step 1: Create Widget Configuration
步骤1:配置小部件
- Log into Cloudflare Dashboard
- Navigate to Turnstile section
- Click "Add Site"
- Configure:
- Widget Mode: Managed (recommended), Non-Interactive, or Invisible
- Domains: Add allowed hostnames (e.g., example.com, localhost for dev)
- Name: Descriptive name (e.g., "Production Login Form")
Key Points:
- Use separate widgets for dev/staging/production
- Restrict domains to only those you control
- Managed mode provides best balance of security and UX
- localhost must be explicitly added for local testing
- 登录Cloudflare控制台
- 导航至Turnstile板块
- 点击“添加站点”
- 配置以下内容:
- 小部件模式:托管模式(推荐)、非交互式模式、隐形模式
- 域名:添加允许的主机名(例如example.com,开发环境添加localhost)
- 名称:描述性名称(例如“生产环境登录表单”)
核心要点:
- 开发/预发布/生产环境使用独立的小部件
- 仅添加你可控的域名到允许列表
- 托管模式在安全性和用户体验间达到最佳平衡
- 本地测试必须显式添加localhost
Step 2: Client-Side Integration
步骤2:客户端集成
Choose between implicit or explicit rendering:
Implicit Rendering (Recommended for static forms):
html
<!-- 1. Load script -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- 2. Add widget -->
<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>Explicit Rendering (For SPAs/dynamic UIs):
typescript
// 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 tokenReact Integration (using @marsidev/react-turnstile):
tsx
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>
)
}选择隐式渲染或显式渲染:
隐式渲染(推荐用于静态表单):
html
<!-- 1. 加载脚本 -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- 2. 添加小部件 -->
<div class="cf-turnstile"
data-sitekey="YOUR_SITE_KEY"
data-callback="onSuccess"
data-error-callback="onError"></div>
<script>
function onSuccess(token) {
console.log('Turnstile验证成功:', token)
}
function onError(error) {
console.error('Turnstile验证错误:', error)
}
</script>显式渲染(适用于单页应用/动态UI):
typescript
// 1. 以显式模式加载脚本
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>
// 2. 程序化渲染小部件
const widgetId = turnstile.render('#container', {
sitekey: 'YOUR_SITE_KEY',
callback: (token) => {
console.log('令牌:', token)
},
'error-callback': (error) => {
console.error('错误:', error)
},
theme: 'auto',
execution: 'render', // 或'execute'用于手动触发验证
})
// 控制小部件生命周期
turnstile.reset(widgetId) // 重置小部件
turnstile.remove(widgetId) // 移除小部件
turnstile.execute(widgetId) // 手动触发验证挑战
const token = turnstile.getResponse(widgetId) // 获取当前令牌React集成(使用@marsidev/react-turnstile):
tsx
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}>提交</button>
</form>
)
}Step 3: Server-Side Validation
步骤3:服务器端验证
MANDATORY: Always call Siteverify API to validate tokens.
typescript
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验证令牌。
typescript
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>()
// 额外验证
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
}
// 在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验证失败', { status: 401 })
}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
1x00000000000000000000AA✅ 调用Siteverify API - 服务器端验证是强制要求
✅ 使用HTTPS - 切勿通过HTTP进行验证
✅ 保护secret key - 切勿在前端代码中暴露
✅ 处理令牌过期 - 令牌5分钟后过期
✅ 实现错误回调 - 优雅处理验证失败
✅ 测试使用虚拟密钥 - 测试sitekey:
✅ 设置合理超时 - 不要无限等待验证结果
✅ 验证action/hostname - 指定时检查额外字段
✅ 定期轮换密钥 - 通过控制台或API轮换secret key
✅ 监控分析数据 - 跟踪验证通过率和失败情况
1x00000000000000000000AANever 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请求
❌ 暴露secret key - 仅在后端环境中存储密钥
❌ 信任客户端验证 - 令牌可能被伪造
❌ 缓存api.js - 未来更新会导致集成失效
❌ 测试使用生产密钥 - 使用虚拟密钥替代
❌ 忽略错误回调 - 必须处理验证失败
Known Issues Prevention
已知问题预防
This skill prevents 12 documented issues:
本技能可预防12个已记录的问题:
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分析仪表板显示令牌验证次数为0
来源: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:Secret Key在前端暴露
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
原因:Secret Key硬编码在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:内容安全策略(CSP)阻止加载
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
原因:未知 - 2025年发现为Cloudflare侧问题
预防方案:模板实现了错误回调、重试逻辑和降级处理
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(2025年4月)
原因: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(2025年10月)
原因:Jest的模块解析问题
预防方案:测试指南包含Jest模拟模式和虚拟sitekey的使用方法
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
Prevention: Templates document single-use constraint and token refresh patterns
success: false错误现象:返回并提示"token already spent"
来源:https://developers.cloudflare.com/turnstile/troubleshooting/testing
原因:每个令牌仅可验证一次
预防方案:模板记录了单令牌单次使用的约束和令牌刷新模式
success: falseConfiguration Files Reference
配置文件参考
wrangler.jsonc (Cloudflare Workers)
wrangler.jsonc(Cloudflare Workers)
jsonc
{
"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"]
}Why these settings:
- for public sitekey (visible in client code)
vars - for private secret key (encrypted, backend-only)
secrets - Use dummy keys for development (see testing-guide.md)
- Rotate production secret keys quarterly
jsonc
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// 公开sitekey(可提交到版本控制)
"vars": {
"TURNSTILE_SITE_KEY": "1x00000000000000000000AA" // 生产环境使用真实密钥
},
// Secret key(禁止提交 - 使用wrangler secret设置)
// 执行命令:wrangler secret put TURNSTILE_SECRET_KEY
"secrets": ["TURNSTILE_SECRET_KEY"]
}配置说明:
- 用于存储公开sitekey(可在客户端代码中可见)
vars - 用于存储私有secret key(加密存储,仅后端可用)
secrets - 开发环境使用虚拟密钥(详见testing-guide.md)
- 每季度轮换生产环境的secret key
Required CSP Directives
必需的CSP指令
html
<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;
">html
<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;
">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') || '')
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('缺少Turnstile令牌', 400)
}
// 验证令牌
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('无效的Turnstile令牌', 401)
}
// 处理登录逻辑
return c.json({ message: '登录成功' })
})
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('请完成验证挑战')
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('提交失败')
return
}
// 提交成功逻辑
}
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('验证挑战失败')}
onExpire={() => setToken(undefined)}
/>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={!token}>
提交
</button>
</form>
)
}适用场景:使用React Hooks的Next.js客户端表单
Pattern 3: E2E Testing with Dummy Keys
模式3:使用虚拟密钥进行端到端测试
typescript
// 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"]', 'test@example.com')
// Turnstile auto-solves with dummy token
await page.click('button[type="submit"]')
await expect(page.locator('.success')).toBeVisible()
})When to use: Automated testing (Playwright, Cypress, Jest)
typescript
// 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('带Turnstile的表单提交', async ({ page }) => {
// 设置测试环境
await page.goto('/contact?test=true')
// 测试模式下小部件使用测试sitekey
await page.fill('input[name="email"]', 'test@example.com')
// Turnstile自动使用虚拟令牌完成验证
await page.click('button[type="submit"]')
await expect(page.locator('.success')).toBeVisible()
})适用场景:自动化测试(Playwright、Cypress、Jest)
Pattern 4: Widget Lifecycle Management
模式4:小部件生命周期管理
typescript
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),
})When to use: SPAs requiring programmatic widget control
typescript
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() // 已渲染则重置
}
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)
}
}
// 使用示例
const manager = new TurnstileManager(SITE_KEY)
manager.render('#container', {
onSuccess: (token) => console.log('令牌:', token),
onError: (error) => console.error('错误:', error),
})适用场景:需要程序化控制小部件的单页应用
Using Bundled Resources
捆绑资源使用说明
Scripts (scripts/)
脚本(scripts/)
- check-csp.sh - Verifies Content Security Policy allows Turnstile scripts and iframes
Example Usage:
bash
./scripts/check-csp.sh https://example.com- check-csp.sh - 验证内容安全策略是否允许Turnstile脚本和iframe
使用示例:
bash
./scripts/check-csp.sh https://example.comReferences (references/)
参考文档(references/)
- - Complete reference of all widget configuration options
references/widget-configs.md - - Comprehensive error code reference with troubleshooting
references/error-codes.md - - Testing strategies, dummy keys, E2E patterns
references/testing-guide.md - - React-specific patterns and @marsidev/react-turnstile usage
references/react-integration.md
When Claude should load these:
- : When configuring widget appearance, themes, or execution modes
widget-configs.md - : When debugging error codes 100*, 200*, 300*, 400*, 600*
error-codes.md - : When setting up E2E tests or local development
testing-guide.md - : When integrating with React, Next.js, or encountering React-specific issues
react-integration.md
- - 完整的小部件配置选项参考
references/widget-configs.md - - 包含故障排除方案的全面错误码参考
references/error-codes.md - - 测试策略、虚拟密钥、端到端测试模式
references/testing-guide.md - - React特定集成模式和@marsidev/react-turnstile使用指南
references/react-integration.md
何时加载这些文档:
- :配置小部件外观、主题或执行模式时
widget-configs.md - :调试100*、200*、300*、400*、600*系列错误码时
error-codes.md - :设置端到端测试或本地开发环境时
testing-guide.md - :与React、Next.js集成或遇到React特定问题时
react-integration.md
Templates (templates/)
模板(templates/)
- - Cloudflare Workers environment configuration
wrangler-turnstile-config.jsonc - - Implicit rendering HTML example
turnstile-widget-implicit.html - - Explicit rendering JavaScript API
turnstile-widget-explicit.ts - - Siteverify API validation function
turnstile-server-validation.ts - - React component using @marsidev/react-turnstile
turnstile-react-component.tsx - - Hono route handler with validation
turnstile-hono-route.ts - - Testing configuration with dummy keys
turnstile-test-config.ts
- - Cloudflare Workers环境配置模板
wrangler-turnstile-config.jsonc - - 隐式渲染HTML示例
turnstile-widget-implicit.html - - 显式渲染JavaScript API示例
turnstile-widget-explicit.ts - - Siteverify API验证函数模板
turnstile-server-validation.ts - - 使用@marsidev/react-turnstile的React组件模板
turnstile-react-component.tsx - - 带验证的Hono路由处理器模板
turnstile-hono-route.ts - - 带虚拟密钥的测试配置模板
turnstile-test-config.ts
Advanced Topics
高级主题
Pre-Clearance for SPAs
单页应用预验证
Turnstile can issue a pre-clearance cookie that persists across page navigations in single-page applications.
typescript
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 }),
})
},
})Turnstile可颁发跨页面导航持久化的预验证Cookie。
typescript
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
// 请求预验证Cookie
await fetch('/api/pre-clearance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
})
},
})Custom Actions and cData
自定义Action和cData
Track different challenge types or pass custom data:
typescript
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
},
})Server-side verification:
typescript
const result = await validateTurnstile(token, secretKey)
if (result.action !== 'login') {
return new Response('Invalid action', { status: 400 })
}
const customData = JSON.parse(result.cdata || '{}')跟踪不同挑战类型或传递自定义数据:
typescript
turnstile.render('#container', {
sitekey: SITE_KEY,
action: 'login', // 在分析数据中跟踪操作类型
cdata: JSON.stringify({ userId: '123' }), // 自定义数据(最大255字符)
callback: (token) => {
// 令牌中包含action和cdata,供服务器验证
},
})服务器端验证:
typescript
const result = await validateTurnstile(token, secretKey)
if (result.action !== 'login') {
return new Response('无效操作', { status: 400 })
}
const customData = JSON.parse(result.cdata || '{}')Retry and Error Handling Strategies
重试与错误处理策略
typescript
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')
}
}typescript
class TurnstileWithRetry {
private retryCount = 0
private maxRetries = 3
render(containerId: string) {
turnstile.render(containerId, {
sitekey: SITE_KEY,
retry: 'auto', // 或'never'用于手动控制
'retry-interval': 8000, // 重试间隔(毫秒)
'error-callback': (error) => {
this.handleError(error)
},
})
}
private handleError(error: string) {
// 无需重试的错误码
const noRetry = ['110100', '110200', '110500']
if (noRetry.some(code => error.includes(code))) {
this.showFallback()
return
}
// 临时错误重试
if (this.retryCount < this.maxRetries) {
this.retryCount++
setTimeout(() => {
turnstile.reset(this.widgetId)
}, 2000 * this.retryCount) // 指数退避
} else {
this.showFallback()
}
}
private showFallback() {
// 显示替代验证方式
console.error('Turnstile验证失败 - 显示降级方案')
}
}Multi-Widget Pages
多小部件页面
typescript
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)typescript
const widgets = {
login: null as string | null,
signup: null as string | null,
}
// 渲染多个小部件
widgets.login = turnstile.render('#login-widget', {
sitekey: SITE_KEY,
action: 'login',
})
widgets.signup = turnstile.render('#signup-widget', {
sitekey: SITE_KEY,
action: 'signup',
})
// 重置特定小部件
turnstile.reset(widgets.login)
// 获取特定小部件的令牌
const loginToken = turnstile.getResponse(widgets.login)Dependencies
依赖说明
Required:
- None (Turnstile loads from CDN)
Optional (React):
- - Official Cloudflare-recommended React integration
@marsidev/react-turnstile@1.3.1 - - TypeScript type definitions
turnstile-types@1.2.3
Optional (Other Frameworks):
- - Vue 3 integration
vue-turnstile - - Alternative Vue 3 wrapper
cfturnstile-vue3 - - Angular integration
ngx-turnstile - - Svelte integration
svelte-turnstile - - Nuxt full-stack integration
@nuxtjs/turnstile
必需依赖:
- 无(Turnstile从CDN加载)
可选依赖(React):
- - Cloudflare官方推荐的React集成库
@marsidev/react-turnstile@1.3.1 - - TypeScript类型定义
turnstile-types@1.2.3
可选依赖(其他框架):
- - Vue 3集成库
vue-turnstile - - 替代Vue 3封装库
cfturnstile-vue3 - - Angular集成库
ngx-turnstile - - Svelte集成库
svelte-turnstile - - Nuxt全栈集成库
@nuxtjs/turnstile
Official Documentation
官方文档
- Cloudflare Turnstile: https://developers.cloudflare.com/turnstile/
- Get Started: https://developers.cloudflare.com/turnstile/get-started/
- Client-Side Rendering: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/
- Server-Side Validation: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
- Error Codes: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes/
- Testing: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
- Community Resources: https://developers.cloudflare.com/turnstile/community-resources/
- Migration from reCAPTCHA: https://developers.cloudflare.com/turnstile/migration/recaptcha/
- Cloudflare MCP: Use tool
mcp__cloudflare-docs__search_cloudflare_documentation
- Cloudflare Turnstile:https://developers.cloudflare.com/turnstile/
- 快速入门:https://developers.cloudflare.com/turnstile/get-started/
- 客户端渲染:https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/
- 服务器端验证:https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
- 错误码:https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes/
- 测试:https://developers.cloudflare.com/turnstile/troubleshooting/testing/
- 社区资源:https://developers.cloudflare.com/turnstile/community-resources/
- 从reCAPTCHA迁移:https://developers.cloudflare.com/turnstile/migration/recaptcha/
- Cloudflare MCP:使用工具
mcp__cloudflare-docs__search_cloudflare_documentation
Package Versions (Verified 2025-10-22)
已验证的包版本(2025-10-22)
json
{
"devDependencies": {
"@marsidev/react-turnstile": "^1.3.1",
"turnstile-types": "^1.2.3"
}
}Notes:
- @marsidev/react-turnstile is Cloudflare's recommended React package
- Last updated September 2025 (actively maintained)
- Compatible with React 18+, Next.js 13+, Next.js 14+, Next.js 15+
json
{
"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+
Production Example
生产环境示例
This skill is based on production implementations:
- Cloudflare Workers: Official HTMLRewriter example
- React Apps: @marsidev/react-turnstile (Cloudflare-verified)
- Community: WordPress, Craft CMS, SilverStripe, Statamic integrations
- Validation: ✅ All 12 known issues documented and prevented
本技能基于以下生产环境实现:
- Cloudflare Workers:官方HTMLRewriter示例
- React应用:@marsidev/react-turnstile(Cloudflare验证)
- 社区集成:WordPress、Craft CMS、SilverStripe、Statamic
- 验证覆盖:✅ 12个已知问题均已记录并提供预防方案
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解决方案:在Cloudflare控制台的小部件配置中添加你的域名(包括开发环境的localhost)。本地开发可使用虚拟测试sitekey 替代。
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.
解决方案:实现带重试逻辑的错误回调。这是2025年发现的Cloudflare侧已知问题。重试失败后显示替代验证方案。
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)
- 验证secret key是否正确
- 确保令牌未被验证过(单令牌单次使用)
- 检查主机名是否与小部件配置匹配
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:
typescript
// jest.setup.ts
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))解决方案:在Jest配置中模拟Turnstile组件:
typescript
// jest.setup.ts
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))Complete Setup Checklist
完整设置检查清单
Use this checklist to verify your setup:
- Created Turnstile widget in Cloudflare Dashboard
- Added allowed domains (including localhost for dev)
- Frontend widget loads from
https://challenges.cloudflare.com/turnstile/v0/api.js - Widget renders with correct sitekey
- Error callback implemented and tested
- Server-side Siteverify validation implemented
- Secret key stored in environment variable (not hardcoded)
- Token validation includes remoteip check
- CSP allows challenges.cloudflare.com (if using CSP)
- Testing uses dummy sitekeys ()
1x00000000000000000000AA - Token expiration handling implemented (5 min TTL)
- Widget accessibility tested (keyboard navigation, screen readers)
- Error states display user-friendly messages
- Production deployment uses separate widget from dev/staging
Questions? Issues?
- Check for specific error troubleshooting
references/error-codes.md - Verify all steps in the 3-Step Setup Process
- Check official docs: https://developers.cloudflare.com/turnstile/
- Ensure server-side validation is implemented (most common issue)
- Use Cloudflare Docs MCP tool:
mcp__cloudflare-docs__search_cloudflare_documentation
Token Efficiency: ~65-70% savings (10-12k tokens → 3-4k tokens)
Errors Prevented: 12 documented issues with complete solutions
使用此清单验证你的Turnstile设置:
- 在Cloudflare控制台创建了Turnstile小部件
- 添加了允许的域名(包括开发环境的localhost)
- 前端小部件从加载
https://challenges.cloudflare.com/turnstile/v0/api.js - 小部件使用正确的sitekey渲染
- 实现并测试了错误回调
- 实现了服务器端Siteverify验证
- Secret key存储在环境变量中(未硬编码)
- 令牌验证包含remoteip检查
- CSP允许challenges.cloudflare.com(如果使用CSP)
- 测试使用虚拟sitekey()
1x00000000000000000000AA - 实现了令牌过期处理(5分钟TTL)
- 测试了小部件的可访问性(键盘导航、屏幕阅读器)
- 错误状态显示用户友好的提示信息
- 生产环境使用与开发/预发布环境独立的小部件
有疑问?遇到问题?
- 查看获取特定错误的故障排除方案
references/error-codes.md - 验证三步设置流程的所有步骤
- 查阅官方文档:https://developers.cloudflare.com/turnstile/
- 确保已实现服务器端验证(最常见问题)
- 使用Cloudflare Docs MCP工具:
mcp__cloudflare-docs__search_cloudflare_documentation
令牌效率提升:约65-70%的令牌消耗减少(10-12k令牌 → 3-4k令牌)
已预防错误:12个已记录的问题,均提供完整解决方案