跳转到主要内容
Guide

cf-turnstile-response invalid:为什么你的 Turnstile token 被拒,以及如何修复

2026-06-057 分钟阅读

如果你的服务端校验一直返回 cf-turnstile-response invalid,你并不孤单。这是开发者在接入 Cloudflare Turnstile 时最常遇到的失败之一,而令人沮丧的是:同一段代码常常上一分钟还能用,下一分钟就失败了。你的小组件生成了 token(也就是 cf-turnstile-response 的值),它到达了你的后端,你把它 POST 到了 Cloudflare 的 /siteverify 接口,结果返回 success: false,并带有诸如 timeout-or-duplicate、invalid-input-response 或 invalid-input-secret 之类的错误码。在客户端,你可能还会看到通用的 600010 challenge 错误。

好消息是:一个无效的 cf-turnstile-response 几乎总能追溯到一份很短、且可修复的根因清单。本指南会逐一讲解每种根因、给出确切的修复方法,并在最后展示自动化场景的可靠方案——也就是当你每次都需要以编程方式获取一个全新有效 token 的时候。

cf-turnstile-response 字段到底是什么

当 Turnstile 小组件在浏览器中运行时,它会执行一次轻量级的 challenge 并生成一个一次性 token。该 token 会被写入一个名为 cf-turnstile-response 的隐藏表单输入框中(同时也会传给你的 JavaScript 回调函数)。它就是“真实浏览器完成了 challenge”的凭证。

你的后端必须取得这个 token,并在服务端进行校验:连同你的 secret key 一起 POST 到 https://challenges.cloudflare.com/turnstile/v0/siteverify。Cloudflare 返回 JSON:成功时为 { "success": true, ... },token 被拒时为 { "success": false, "error-codes": [...] }。所谓 “cf-turnstile-response invalid” 问题,本质上就是一次非成功的 siteverify 响应,而 error-codes 数组会准确告诉你原因。

根因 #1:token 已过期或已被使用(timeout-or-duplicate)

这是 turnstile token 无效最常见的原因。Turnstile token 是单次使用且生命周期很短的:每个 token 大约有效 300 秒(5 分钟),且只能被校验一次。提交两次,或在有效窗口关闭之后再提交,siteverify 就会返回 error-codes: ["timeout-or-duplicate"]。

在现实中它出现的频率比你想象的更高:用户打开表单后分了神、上传了一个很大的附件,或在缓慢的网络上提交,结果 token 在传输途中就过期了。又或者你的代码对失败请求做了重试,意外地用同一个 token 重新校验了一次。再或者一个被双击的提交按钮发出了两个请求。

  • token 过期后,用 JavaScript 回调 turnstile.reset() 刷新小组件(或重新渲染一个新组件),确保用户始终提交的是当前有效的 token。
  • 每个 token 只校验一次。永远不要用同一个 cf-turnstile-response 调用 /siteverify 两次。
  • 首次点击后禁用提交按钮,避免重复 POST。
  • 如果表单可能长时间停留打开,设置 expired-callback,在提交前重新执行 challenge。

根因 #2:sitekey 与 secret 不匹配(invalid-input-secret)

你的前端用一个公开的 sitekey 渲染小组件;你的后端用一个私有的 secret key 进行校验。这两者必须属于同一个 Cloudflare 账户下的同一个 Turnstile 小组件。如果你把一个组件的 sitekey 和另一个组件的 secret 混用,或者把测试环境和生产环境的密钥弄反了,那么每一次校验都会失败。此时经典的 siteverify 错误码是 invalid-input-secret(当 secret 为空时则是 missing-input-secret)。

修复方法:打开 Cloudflare 控制台,找到确切的那个小组件,从同一个组件中复制 sitekey 和 secret。把 secret 存进环境变量,并确认它在运行时确实被加载了(环境变量缺失是一个出人意料地常见的元凶)。同时确保你是把 secret 作为 secret 这个表单字段发送的,而不是误发了 sitekey。

根因 #3:hostname、action 或 cData 不匹配

一个 Turnstile 小组件会绑定到你为它配置的域名。如果渲染该组件的页面所在的 hostname 不在组件允许的范围内,token 就会被拒。siteverify 响应中包含 hostname 字段,以及 action 和 cdata 字段,你可以也应该把它们与你的预期进行比对。

需要检查的三类不匹配。第一,hostname:确保提供该组件的域名(包括子域名以及开发期间的 localhost)在组件的允许列表中,并把返回的 hostname 与你自己的域名做对比。第二,action:如果你在组件上设置了 data-action,校验 siteverify 返回的 action 是否一致;不一致就按失败处理。第三,cData:如果你通过 data-cdata 传入了自定义数据,校验返回的 cdata。这些检查还能加固防御,避免 token 在不同接口之间被重复使用。客户端的 600010 错误经常正是源于这一类配置错误(域名未被允许、为该环境使用了错误的密钥)。

根因 #4:测试用 dummy 密钥、幂等性与时钟偏差

有几个更隐蔽的原因常让经验丰富的开发者也措手不及。

在生产环境使用测试 sitekey:Cloudflare 的 dummy 密钥(例如永远通过的 sitekey 1x00000000000000000000AA)会生成一个 dummy token。真实的生产 secret key 会拒绝这个 dummy token,反之亦然——测试用的 secret 会拒绝真实 token。如果你在构建中留下了测试密钥,请把它换成你真实组件的密钥。

idempotency_key 复用:siteverify 接受一个可选的 idempotency_key,这样你就能安全地对同一个 token 重试校验并得到缓存结果。但如果你对一个不同的 token 复用了同一个 idempotency_key,你拿回的会是缓存的(旧的)结果,看起来就像一个错误或无效的响应。请为每个 token 使用一个全新的 idempotency_key,或者干脆不传。

时钟偏差:token 过期是基于时间的,因此服务器时钟漂移会让全新的 token 看起来已过期(或错误地延长有效窗口)。请用 NTP 保持服务器时间同步。

快速排查清单

在改代码之前,先读一读你 siteverify 响应中的 error-codes 数组。它通常会直接点明问题所在:

  • timeout-or-duplicate -> token 已过期(超过约 300 秒)或被校验了不止一次。换一个全新 token;只校验一次。
  • invalid-input-response -> cf-turnstile-response 格式错误、为空,或在传输途中过期。确认该字段确实被发送了。
  • invalid-input-secret / missing-input-secret -> secret 错误或缺失。重新复制对应组件的 secret。
  • 客户端显示 600010 -> 环境/配置问题(为该域名使用了错误密钥、域名未被允许、被浏览器扩展拦截)。重新核对密钥和允许的 hostname。

当你需要以编程方式获取有效 token 时:NSLSolver

以上所有内容修复的都是面向真人的合法集成。但如果你在做自动化——运行爬虫、机器人、针对第三方站点的集成测试,或压测工具——那就没有真人来完成这个小组件,你也无法自己铸造一个有效的 cf-turnstile-response。你需要一个能驱动真实 challenge、并返回一个可供你提交的全新一次性 token 的服务。

这正是 NSLSolver 所做的。你把目标站点的 sitekey 和 URL POST 到 /solve,它会返回一个有效的 Turnstile token,你像浏览器一样把它用作 cf-turnstile-response 的值。token 平均约在 250ms 内返回,成功率 99.9%,且失败的求解不计费。Turnstile 求解价格为每 1,000 次 0.40 美元,按量付费,新账户在注册时可获得 100 次免费请求,无需信用卡、无需加密货币即可开始。

由于 NSLSolver 返回的每个 token 都是全新的,只要你及时提交(在约 300 秒的窗口内)且只提交一次,你就能彻底绕开单次使用和过期的问题。

下面是一个完整的 Python 示例,它先求解 Turnstile challenge,然后把 token 提交到目标表单:

solve_turnstile.py
import requests

NSL_API_KEY = "nsl_YOUR_API_KEY"
TARGET_URL = "https://target-site.com"
SITE_KEY = "0x4AAAAAAA..."  # the data-sitekey on the target page

# 1) Ask NSLSolver for a fresh, valid Turnstile token
resp = requests.post(
    "https://api.nslsolver.com/solve",
    headers={"X-API-Key": NSL_API_KEY},
    json={
        "type": "turnstile",
        "site_key": SITE_KEY,
        "url": TARGET_URL,
    },
    timeout=120,
)
resp.raise_for_status()
token = resp.json()["token"]  # e.g. "0.AAAA..."

# 2) Submit the token as the cf-turnstile-response value
# Do this promptly (within ~300s) and only once per token.
submit = requests.post(
    f"{TARGET_URL}/login",
    data={
        "username": "[email protected]",
        "password": "secret",
        "cf-turnstile-response": token,
    },
    timeout=30,
)
print(submit.status_code, submit.text[:200])

常见问题

为什么我全新的 cf-turnstile-response 仍然返回 timeout-or-duplicate?

几乎总是因为该 token 被校验了不止一次(一次重试、一次被双击的提交,或两个并行请求),或者它在到达 siteverify 之前停留太久了。Turnstile token 是单次使用的,有效期约 300 秒。请确保每个 token 只校验一次并及时提交;如果表单长时间打开,请在提交前 reset 小组件以铸造一个新 token。

invalid-input-response 和 invalid-input-secret 有什么区别?

invalid-input-response 表示你发送的 token(即 cf-turnstile-response)缺失、格式错误或已过期。invalid-input-secret 表示你用于校验的 secret key 错误,或与签发该 token 的组件不匹配。前者是 token 问题;后者是密钥配置问题。

如何修复 Turnstile 错误 600010?

600010 是一个通用的客户端 challenge 失败,通常由配置或环境问题导致:sitekey 与域名不匹配、域名不在组件的允许列表中、为该环境使用了错误的密钥,或某个浏览器扩展在干扰。请重新核对 sitekey 和 secret 是否属于同一个组件、你的 hostname 是否被允许,然后在一个干净的浏览器中测试。

我能为自动化生成一个有效的 cf-turnstile-response token 吗?

无法自己生成——token 必须来自对真实 challenge 的求解。对于爬虫、机器人和自动化测试,你可以把目标站点的 site_key 和 url POST 到 NSLSolver 的 /solve 接口(type 为 turnstile),即可获得一个全新有效的 token,用作 cf-turnstile-response 的值。请只提交一次,并在 300 秒窗口内提交。

别再和无效的 Turnstile token 较劲

按需获取全新有效的 cf-turnstile-response token。注册即送 100 次免费请求,无需信用卡。Turnstile 求解低至每 1,000 次 0.40 美元,平均约 250ms,且只为成功的求解付费。