博文:踩坑记录|Cloudflare Turnstile 抓包看到 /fo 接口 POST 400 Bad Request,别再搞错接口了

15次阅读
没有评论

标签:Cloudflare、Turnstile、人机验证、爬虫、后端开发

最近在对接、调试 Cloudflare Turnstile 人机验证的时候,抓包经常看到一个长长的 challenge 地址,POST 请求直接返回 400 Bad Request,一开始误以为这个就是用来校验 Token 的接口,踩了不小的坑,这里整理一下原理、区别和排坑方案。

先放报错信息

请求地址(示例)

https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/b/fo/3920876840:1794233415:H7pn5yv2WvDoTkhOWR5xDE_YclOLcW3VxbZAvaNuU8M/a57d90e17ea3ce5d/Cu9IGwdfUSPi_jYX7P3-w9zzJQKfBiFLHMpeLoHT9vjLkVQdvxtrZisuW7IzCQYn2W9KCZXUxXLLLs8SF5v5K6j9pJwutSwBOqushEUvioclE7prEZumjtE9Fb9Dy4jmUg
  • 请求方式:POST
  • HTTP 状态:400 Bad Request
  • 请求体一般是类似这种加密载荷:{"d":"xxx==$xxx=="}

很多人第一反应:我直接复制这个请求,后端重放调用,是不是就能拿到验证结果?答案是:不行,这么做一定会 400。

两个极易混淆的接口,一定要分清

这是最核心的知识点,也是绝大多数人踩坑的根源:

表格

接口地址 作用 调用方 业务能不能手动调用
/cdn-cgi/challenge-platform/h/b/fo/... 浏览器提交环境指纹、浏览器特征,内部交互生成 cf-turnstile-response Turnstile 前端 JS 自动发起 ❌ 不可以,禁止手动重放
https://challenges.cloudflare.com/turnstile/v0/siteverify 后端校验 cf-turnstile-response 令牌真伪 你的业务服务端 ✅ 这才是业务对接的标准接口

/fo 属于 Cloudflare Challenge 平台的内部交互接口,不是开放校验接口。 我们业务开发只需要关心 siteverify,前端只需要嵌入官方 Turnstile Widget,剩下的 /fo 相关通信全部交给官方 JS 自行处理。

为什么这个 /fo 接口抓包重放就 400?

  1. 一次性上下文,不可重放 URL 路径里面自带 challenge ID、时间戳、一次性会话标识;请求体里 d 字段是加密二进制载荷,内置 nonce、时间、浏览器指纹,单次有效,过期直接失效。抓包复制的 URL + Body,第二次提交直接 400。
  2. 请求环境绑定 Turnstile 的 JS 会在浏览器环境采集大量浏览器原生 API 指纹,无头、缺少浏览器环境、缺少对应 Sec-* 请求头、UA 异常,生成的 payload 本身就是非法的,CF 校验不通过返回 400。
  3. 报文头冲突或者畸形 同时携带 Content-Length 和 Transfer-Encoding: chunked、代理篡改 body、base64 转义错误,都会导致报文解析失败,返回 400。
  4. 客户端时间偏差太大 载荷内置时间戳,如果运行机器系统时钟和标准时间差太多,Cloudflare 判定请求非法。

两种场景的处理方案

场景一:正常业务接入(网站登录、表单防刷)

✅ 正确做法:

  1. 页面嵌入 Turnstile 官方前端组件,JS 自动处理所有 /fo 这类内部通信,你不需要关心这个接口;
  2. 用户完成验证后,拿到表单里的 cf-turnstile-response;
  3. 后端调用 siteverify 接口校验这个 token。

后端 Node.js 最简校验代码示例:

async function verifyToken(cfTurnstileResponse, remoteIp) {
  const body = new URLSearchParams({
    secret: process.env.TURNSTILE_SECRET_KEY,
    response: cfTurnstileResponse,
    remoteip: remoteIp
  })
  const resp = await fetch(process.env.TURNSTILE_VERIFY_URL, {
    method: "POST",
    body
  })
  return await resp.json()
}

配套环境变量 .env

# Turnstile 前端公开密钥
TURNSTILE_SITE_KEY=your-turnstile-site-key
# 后端私钥,严禁暴露前端
TURNSTILE_SECRET_KEY=your-turnstile-secret-key
# 可选,限制校验域名,防盗用
TURNSTILE_EXPECTED_HOSTNAME=verify.example.com
# 校验接口地址,一般不用修改
TURNSTILE_VERIFY_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify

场景二:自动化 / 爬虫逆向场景

❌ 误区:抓包 /fo 请求直接重放 ✅ 正确思路:必须完整加载并运行 Turnstile 官方 JS,在完整浏览器上下文(Playwright / Puppeteer 等)中,让 JS 动态生成每一轮的 challenge、payload 和 token,不能复用抓包数据。

快速排查清单(出现 400 时对照)

  1. 确认你是不是手动调用了 /fo 接口,如果是,立刻停止;业务校验只用 siteverify
  2. 如果是前端页面控制台报这个 400:大概率是浏览器扩展、脚本篡改了 Turnstile JS,刷新页面测试
  3. 同步客户端系统时间,避免时钟偏移
  4. 检查请求头,不要混用 Content-Length 和 chunked 编码
  5. 自动化场景优先使用完整浏览器环境,不要尝试逆向重放 challenge 内部接口

最后小结

/cdn-cgi/challenge-platform/h/b/fo/xxx 是 Turnstile 内部 challenge 通信通道,设计上就不对外提供调用。 很多新手、做自动化的同学抓包看到这个接口就想直接拿来用,结果反复 400,浪费大量调试时间。

记住一句话:前端交给官方 JS,后端只调用 siteverify 校验 token,不要碰 /fo 接口。

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 1
评论(没有评论)
验证码