验证码 token 已过期:原因与解决方法
验证码 token 是一种短期有效的凭证,用于证明挑战已经通过。Scarnox 的 PopularCaptcha token 在签发后约两分钟过期。如果你在这一时间窗口之后提交 token,网站会拒绝表单,通常会提示验证码已过期或无效。问题的修复几乎总是与时机有关:尽量靠近提交时再求解验证码,每个 token 只使用一次,并且只有在确认第一次尝试未被接受时才重试。
为什么 token 会过期
token 并不是永久有效的凭证。网站希望确认是一个人(或代表其行事的求解器)最近完成了挑战,因此给 token 设定了较短的有效期。计时从 token 签发时开始,而不是从你首次调用 API 时开始。在求解与提交之间发生的任何事情,都会消耗这两分钟。
常见原因
- 过早求解。 预先求解一批 token 并留待以后使用,是最常见的原因。等到 token 被使用时,它已经过时了。
- 求解与提交之间的代码缓慢。 一次耗时的数据库写入、队列操作,或放在表单提交之前的
time.sleep,都可能让 token 超出有效期。 - 重复使用 token。 已被接受的 token,或已被失败尝试使用过的 token,不会再次生效。请将每个 token 视为一次性的。
- 重新加载页面。 如果在求解之后刷新了页面,表单可能会发起新的挑战,从而使你手中的 token 失效。
解决办法:在提交前立即求解
最简单可靠的做法,是把求解和提交合并为一步:请求一个 token,把它放入表单,提交表单,并且在两者之间不做任何耗时的操作:
import requests
API_KEY = "scarnox_YOURAPIKEY"
CREATE_URL = "https://api.scarnox.com/api/tasks/create"
def fresh_token(sitekey: str, page_url: str) -> str:
r = requests.post(
CREATE_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"captcha_type": "PopularCaptchaTaskProxyless",
"site_key": sitekey,
"site_url": page_url,
},
timeout=185, # 调用会阻塞直到 token 准备好
)
data = r.json()
if data.get("errorId") != 0:
raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
return data["solution"]["token"]
def submit_form(page_url: str, sitekey: str, form: dict) -> requests.Response:
token = fresh_token(sitekey, page_url) # 在提交前求解
return requests.post(
page_url,
data={**form, "h-captcha-response": token, "g-recaptcha-response": token},
timeout=30,
)
注意,submit_form 并不会把 token 保存在缓存或队列中。它请求一个 token,提交它,然后继续。失败的求解不收费,因此失败的请求在求解这一侧不会产生费用。
安全地重试
有些网站会以明确的响应拒绝过期的 token。另一些网站则会返回一个正常的页面,其中包含错误提示,因此仅凭状态码无法判断。请查看网站对过期 token 实际返回的响应,并据此编写检查逻辑。重试循环可以像这样:
def submit_with_retry(page_url: str, sitekey: str, form: dict, attempts: int = 2):
for _ in range(attempts):
resp = submit_form(page_url, sitekey, form)
if resp.status_code < 400 and "captcha" not in resp.text.lower():
return resp # 按照该网站的响应判断,已被接受
raise RuntimeError("captcha token rejected after retries")
对于会创建内容的表单,重试时要格外小心。如果第一次尝试实际上已被接受,而你的代码误读了响应,那么重试就可能创建重复的账户、订单或消息。只有在你确定第一次 POST 未被接受时才重试,并且针对你所处理的网站编写具体的检查逻辑。
保持时间安排合理
- 不要为了以后使用而循环求解一批 token。应当每次提交前求解。
- 尽量减少求解与提交之间的工作量。
- 对求解请求使用约 185 秒的超时,因为调用会阻塞直到 token 准备好。你为自己的表单提交设置的计时器应远短于 token 的有效期。
- 如果你需要更快的流水线,请参阅验证码求解提速,其中介绍了在不存储 token 的前提下进行并行求解的方法。
常见错误
- 将 token 缓存在字典或 Redis 中以备后用。 这种缓存看起来无害,直到某个 token 在有效期之后才被使用,此时每次提交都会失败。
- 每个会话只求解一次。 长时间存活的会话依然需要为每次表单提交获取新的 token,因为每次提交都可能进行独立的挑战检查。
- 在求解与提交之间休眠。 在求解之后放置的速率限制器或刻意停顿,会消耗掉 token 的有效时间。应把所有节流操作放在求解之前。
- 把所有失败都一视同仁。 你自己这一端的超时、被拒绝的 token,以及网站错误,需要不同的处理方式。请分别记录它们。
记录足够的日志以便诊断
当提交失败时,请记录 token 的签发时间、表单的提交时间、HTTP 状态码,以及响应的前几百个字符。有了这四项信息,你就能一眼看出是 token 过期了,还是网站因其他原因拒绝了它。不要在共享日志中记录完整的 token,因为一个有效的 token 就是一次挑战的凭证。
如果仍然失败,请检查以下几点
- 打印 token 返回与表单提交之间的时间间隔。
- 确认
site_url与显示验证码的页面一致。 - 确认 sitekey 是从当前页面读取的,而不是旧副本。
- 确认表单字段名称。PopularCaptcha 的 token 应放入
h-captcha-response,通常也应放入g-recaptcha-response。
完整的错误响应列表以及每种错误的处理方式,请参阅验证码求解器错误处理。