Troubleshooting

Cloudflare Turnstile 令牌到期时间和竞争条件

本地测试正常,一到生产环境就频繁报“验证失败”?十有八九是 Turnstile token 过期了。Cloudflare 给每个 token 设定了固定有效期,拿到 token 和提交表单之间拖得太久,站点就会拒绝它。下面拆解过期规律、竞态条件从哪来,以及怎么在代码里避免。

Turnstile token 能活多久

Turnstile token 在生成后大约 300 秒(5 分钟) 内有效,比 reCAPTCHA 的约 120 秒宽松不少,但复杂流程里照样会踩坑。

验证码类型 token 有效期
reCAPTCHA v2/v3 约 120 秒
Cloudflare Turnstile 约 300 秒
hCaptcha 约 120 秒

计时器从 Cloudflare 生成 token 那一刻启动,不是 CaptchaAI 返回结果、也不是你的代码收到响应的时候——这是竞态条件的根源。

竞态条件到底是怎么发生的

把整条链路摊开看:

Time 0:00  — You submit a Turnstile task to CaptchaAI
Time 0:15  — CaptchaAI begins solving
Time 0:20  — Token is generated (timer starts here)
Time 0:25  — CaptchaAI returns token to you
Time 0:25+ — Your code processes the token
Time ???   — Your code submits the token to the site

时钟从 0:20 秒开始跑,理论上到 5:20 都能提交,但放进真实业务流程完全是另一回事:

Time 0:20  — Token generated
Time 0:25  — Received by your code
Time 0:30  — Fill form fields
Time 0:35  — Navigate to next page
Time 1:00  — Handle additional dialogs
Time 2:00  — Wait for page load
Time 4:00  — Network latency spike
Time 5:30  — Submit token → EXPIRED

三个最容易触发过期的场景

三种写法最容易让 token 提前失效:

  1. 多步骤长表单 — 很多表单要走完好几页才能最终提交:
Step 1: Fill personal info → Step 2: Fill address → 
Step 3: Solve CAPTCHA → Step 4: Review → Step 5: Submit

如果验证码在第 3 步,提交却在第 5 步,中间耗时很容易超过 5 分钟。跨境电商自动化团队最常踩这个坑:填地址、选支付、过 Turnstile、确认订单,每步都有加载延迟,真正提交时 token 早已过期。

  1. 批量任务提前把 token 解出来 — 先统一解出所有 token,留着后面用:
# DON'T: Solve all tokens first, then use them
tokens = []
for url in urls:
    tokens.append(solve_turnstile(url))  # Tokens age while waiting

for url, token in zip(urls, tokens):
    submit_form(url, token)  # Early tokens may be expired

队列越长,最早解出来的 token 等待时间就越久,轮到提交时大概率已经过期。

  1. 提交失败后拿旧 token 重试 — 表单提交失败,直接拿同一个 token 再试一次:
token = solve_turnstile(site_key, page_url)

for attempt in range(3):
    result = submit_form(page_url, token)
    if result.ok:
        break
    # BUG: Retrying with the same token — it may be expired OR already consumed

问题有两层:token 可能已过期,也可能虽未过期但已被站点标记为“已消费”——两种情况都会导致重试失败。

怎么彻底避免 token 过期

对应的修复策略:

  1. 拿到 token 立刻用(即时解决) — 只在真正准备提交的那一刻才去请求 token:
import requests
import time

def solve_turnstile(site_key, page_url):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "turnstile",
        "sitekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

# Complete all form steps FIRST
fill_personal_info()
fill_address()
navigate_to_review()

# THEN solve and submit immediately
token = solve_turnstile(site_key, page_url)
submit_form(token)  # Submit within seconds of receiving the token
  1. 给 token 挂上“保质期”标记 — 流程有分支、无法保证拿到 token 后立刻提交时,给每个 token 记一个创建时间戳,提交前检查是否还新鲜:
import time

class TimedToken:
    def __init__(self, token, created_at=None):
        self.token = token
        self.created_at = created_at or time.time()
        self.max_age = 270  # 4.5 min — safety margin from 5 min limit

    @property
    def is_valid(self):
        return (time.time() - self.created_at) < self.max_age

    @property
    def remaining_seconds(self):
        return max(0, self.max_age - (time.time() - self.created_at))

# Usage
timed_token = TimedToken(solve_turnstile(site_key, page_url))

# Check before using
if timed_token.is_valid:
    submit_form(timed_token.token)
else:
    # Solve a fresh token
    timed_token = TimedToken(solve_turnstile(site_key, page_url))
    submit_form(timed_token.token)
  1. 重试前重新解一个新 token(JavaScript) — JavaScript 端同理,每次重试都应该重新请求一次,而不是复用上一次的结果:
async function submitWithFreshToken(siteKey, pageUrl, formData) {
  const maxRetries = 3;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    // Always solve a fresh token for each attempt
    const token = await solveTurnstile(siteKey, pageUrl);

    const response = await fetch(pageUrl, {
      method: 'POST',
      body: JSON.stringify({ ...formData, 'cf-turnstile-response': token }),
      headers: { 'Content-Type': 'application/json' }
    });

    if (response.ok) return await response.json();

    console.log(`Attempt ${attempt + 1} failed, solving fresh token...`);
  }

  throw new Error('All attempts failed');
}

怎么判断 token 是不是过期了

站点通常不会明确告诉你“token 已过期”,但这些信号基本可以确认:

信号 说明
提交 token 后返回 HTTP 403 token 无效或已过期
被重定向回表单页 token 校验失败
报错信息“验证失败” 笼统的失败提示,很可能是过期导致
验证码挑战页重新出现 token 被拒绝,Cloudflare 重新发起挑战

打日志定位问题

遇到间歇性失败,先把 token 的年龄记下来,而不是靠猜:

import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("turnstile")

token_received_at = time.time()
token = solve_turnstile(site_key, page_url)
logger.info(f"Token received, length: {len(token)}")

# ... workflow steps ...

submit_time = time.time()
age = submit_time - token_received_at
logger.info(f"Submitting token, age: {age:.1f}s")

if age > 270:
    logger.warning(f"Token may be expired (age: {age:.1f}s > 270s safety limit)")

浏览器端会自动刷新,纯 API 自动化不会

在基于浏览器的正常使用场景里,Turnstile 组件会在 token 过期前自动刷新,expired-callback 会在 token 失效时触发:

turnstile.render('#captcha', {
  sitekey: '0x4AAAA...',
  callback: (token) => {
    console.log('New token:', token);
  },
  'expired-callback': () => {
    console.log('Token expired — widget will auto-refresh');
  }
});

纯 API 自动化没有浏览器渲染 Turnstile 组件,享受不到这个自动刷新——token 新鲜度得自己管理。

常见问题

Turnstile token 的有效期能延长吗?

不能,过期时间由 Cloudflare 服务端控制,客户端和 API 调用方都无法修改,唯一的办法是解一个新 token。

300 秒这个数字准不准?

官方给的是近似值,Cloudflare 可能按站点配置微调。实际开发中把 270 秒(4.5 分钟)当硬性上限,超过就直接重新解一次,别赌运气。

第一次提交成功,为什么重试总是失败?

大概率是复用了同一个 token。Turnstile token 只能被消费一次,哪怕没过期,站点也会拒绝重复提交——每次重试单独解一个新 token 即可。

reCAPTCHA 和 Turnstile 的 token 有效期一样长吗?

不一样。reCAPTCHA v2/v3 约 120 秒就过期,Turnstile 约 300 秒,窗口更宽松,但处理逻辑通用:都要遵循“即时解决、即时提交”。

延伸阅读

用 CaptchaAI 解决 token 过期问题

想告别 token 过期导致的验证失败?注册 CaptchaAI 获取 API Key,把即时解决和追踪年龄落地到自动化流程里,能明显降低这类间歇性报错。

该文章已禁用评论。