在测试中有效的 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%。