故障排查

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

在测试中有效的 Turnstile 令牌在生产中失败。原因:令牌过期。 Turnstile 令牌的生命周期有限,如果您的工作流程在接收令牌和提交令牌之间花费的时间太长,则站点会拒绝它。以下是如何正确处理时间的方法。

令牌生命周期

Turnstile 令牌在创建后大约 300 秒(5 分钟) 到期。这比 reCAPTCHA 的约 120 秒更加慷慨,但在复杂的工作流程中仍然会出现竞争条件。

验证码类型 令牌生命周期
reCAPTCHA v2/v3 约 120 秒
Cloudflare Turnstile ~300秒
验证码 约 120 秒

计时器在 Cloudflare 生成令牌时启动,而不是在 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

常见的竞态条件场景

1. 多步骤表格

最终提交前需要几页的表格:

Step 1: Fill personal info → Step 2: Fill address → 
Step 3: Solve CAPTCHA → Step 4: Review → Step 5: Submit

如果验证码在第 3 步,但在第 5 步提交,则解决和提交之间的延迟可能会超过 5 分钟。

2. 批处理队列

提前解决代币,稍后使用:

# 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

3. 使用旧令牌重试循环

提交失败后重用令牌:

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

防止过期

策略 1:及时解决问题

仅当您准备好提交时才请求令牌:

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

策略 2:追踪代币年龄

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)

策略 3:重试时新鲜令牌 (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');
}

检测过期令牌

该网站通常不会明确告诉您“令牌已过期”。寻找这些信号:

信号 指示
提交令牌后 HTTP 403 令牌无效或已过期
重定向回表单页面 令牌验证失败
错误消息:“验证失败” 一般失败——可能是过期
挑战页面重新出现 令牌被拒绝,Cloudflare 重新提出质疑

记录诊断

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)")

Turnstile 的自动刷新行为

在基于浏览器的流程中,Turnstile 小部件会在令牌过期之前自动刷新令牌。当令牌过期时,data-expired-callback 会触发:

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

在仅 API 自动化(无浏览器)中,您无法从自动刷新中受益。您必须自己管理令牌的新鲜度。

故障排除

问题 原因 处理方式
令牌在测试中有效,但在生产中无效 生产流程较慢 及时解决,而不是提前解决
第一次提交成功,重试失败 重用消耗的代币 为每次尝试解决一个新的令牌
长表格间歇性失败 令牌在多步骤流程期间过期 将验证码解决移至最后一步
批量作业失败率高 批量解决的代币在使用前就会过期 按需解决代币,而不是批量解决代币

常问问题

我可以延长 Turnstile 令牌的使用寿命吗?

不可以。过期时间由 Cloudflare 设置且无法修改。你唯一的选择是解决一个新的令牌。

300 秒的限制有多准确?

这是近似值。 Cloudflare 可能会根据配置调整时间。使用 270 秒(4.5 分钟)作为安全裕度的实际最大值。

我应该QA 预测试令牌以节省时间吗?

仅当您的工作流程可以在几分钟内使用它们时。对于批处理,按需解决令牌而不是提前解决。

相关文章

下一步

防止 Turnstile 令牌过期 —获取您的 CaptchaAI API 密钥并实施即时解决,成功率100%。

该文章已禁用评论。