什么是请求头顺序
一个 HTTP 请求不仅由 URL 和请求体组成,还包含一组请求头:
User-Agent
Accept
Accept-Language
Cookie
...
服务器不仅会读取请求头的内容——它还会看到它们的顺序。
为什么这对反机器人保护很重要
现代保护系统(Cloudflare、DataDome、Imperva 等)会分析:
- TLS 指纹(JA3/JA3S)
- HTTP/2 SETTINGS 指纹(Akamai H2 fingerprint)
- User-Agent
- IP 声誉(ASN、数据中心 IP 或住宅 IP)
- JavaScript 指纹
- 请求头顺序
- 客户端行为
真实浏览器(Chrome、Firefox 等)发送请求头时会:
但常见的 HTTP 库——requests(Python)和 axios(Node.js)——可能会在用户定义的请求头之外添加默认请求头(User-Agent, Accept-Encoding, Connection),对请求头进行排序,或以随机顺序发送它们,这会破坏预期的顺序并暴露自动化特征。
这与 CapMonster Cloud 有什么关系
CapMonster Cloud 只负责求解 CAPTCHA,并不会让你的请求“看起来像浏览器发出的”。
也就是说:
- 你收到了一个有效的 token
- 把它添加到了请求中
- 但服务器仍然返回错误
为什么?因为你的请求看起来像一个机器人。
浏览器如何发送请求头(Chrome 146*)
在真实浏览器中,这种顺序几乎每次都会重复出现:
* 在撰写本文时,Chrome 146 是当前版本。
HTTP/2 伪请求头
:method
:authority
:scheme
:path
常规请求头(Chrome 示例)
sec-ch-ua
sec-ch-ua-mobile
sec-ch-ua-platform
upgrade-insecure-requests
user-agent
accept
sec-fetch-site
sec-fetch-mode
sec-fetch-user
sec-fetch-dest
accept-encoding
accept-language
priority
一个常见错误——从 DevTools 复制
很多开发者会这样做:
- 打开 Chrome DevTools
- 复制请求头
- 将它们粘贴到代码中
问题在于:
- DevTools 并不总是显示真实顺序,而且可能会对请求头进行排序
- 因此,代码中的顺序会与浏览器中的顺序不同
如何获取正确的顺序
使用能够显示真实请求的工具:
- Charles Proxy
- mitmproxy
- powhttp
它们显示的请求头顺序与服务器看到的完全一致。
例如(使用 Charles Proxy 抓取):
:method: GET
:authority: example
:scheme: https
:path: /assets/js/example.js
sec-ch-ua-full-version-list: "Chromium";v="146.0.7680.178", "Not-A.Brand";v="24.0.0.0", "Google Chrome";v="146.0.7680.178"
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"
sec-ch-ua-bitness: "64"
sec-ch-ua-model: ""
sec-ch-ua-mobile: ?0
sec-ch-ua-arch: "x86"
sec-ch-ua-full-version: "146.0.7680.178"
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
sec-ch-ua-platform-version: "19.0.0"
accept: */*
sec-fetch-site: same-origin
sec-fetch-mode: no-cors
sec-fetch-dest: script
referer: https://example.com/
accept-encoding: gzip, deflate, br, zstd
accept-language: en-US,en;q=0.9,ru;q=0.8
priority u=0, i
如何在代码中复现请求头顺序
一旦你拿到了真实的请求头顺序,下一步任务就是在 HTTP 客户端中正确复现它。
关键问题在于,大多数标准库都不保证请求头顺序。
工具 | 顺序控制 | HTTP/2 | TLS 指纹 |
requests | ❌ | ❌ | ❌ |
axios | ❌ | 部分支持 | ❌ |
tls-client | ✅ | ✅ | ✅ |
Playwright | ✅(浏览器) | ✅ | ✅ |
为了准确复现浏览器行为,建议使用专门的解决方案:
- tls-client(Go / Python)——通过 Go 绑定层的 header_order + pseudo_header_order 实现请求头顺序控制 bogdanfinn/tls-client,并将这些参数直接传递给原生库
- 自定义 HTTP/2 客户端
- 浏览器自动化(例如 Playwright)——如果可以接受其额外开销
配置示例(Python + tls-client)
下面的示例演示了复现类似 Chrome 的请求头顺序的一种基础方法。
重要提示:chrome_146 是 Go 库 bogdanfinn/tls-client 中的有效 profile,文档中标注为 “Latest”。不过,标准 Python 包 FlorianREGAZ/Python-Tls-Client(pip install tls-client)已经停止维护,且只包含到 chrome_120 为止的 profile。读者如果安装标准包,将会收到错误。请使用当前的 bogdanfinn/tls-client 分支,或使用支持 chrome_146 的兼容 Python 绑定。
import tls_client
session = tls_client.Session(
client_identifier="chrome_146",
random_tls_extension_order=True
)
session.pseudo_header_order = [
":method",
":authority",
":scheme",
":path",
]
session.header_order = [
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"upgrade-insecure-requests",
"user-agent",
"accept",
"sec-fetch-site",
"sec-fetch-mode",
"sec-fetch-user",
"sec-fetch-dest",
"accept-encoding",
"accept-language",
"cookie",
"priority",
]
headers = {
"sec-ch-ua": '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/146.0.0.0 Safari/537.36",
"accept": "*/*",
"sec-fetch-site": "same-origin",
"sec-fetch-mode": "no-cors",
"sec-fetch-dest": "script",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "en-US,en;q=0.9",
"priority": "u=0, i",
}
response = session.get("https://example.com", headers=headers)
print(response.status_code)
session.header_order 中的顺序必须与您从真实浏览器中获取到的顺序一致。任何偏差都可能影响结果。
HTTP/2 SETTINGS 指纹
反机器人系统分析的不仅仅是请求头——它们甚至会在解析请求头之前检查 HTTP/2 SETTINGS 指纹。Cloudflare 和 DataDome 使用所谓的 Akamai H2 fingerprint,它会将各项参数编码为如下格式的字符串:
SETTINGS|WINDOW_UPDATE|PRIORITY|PSEUDO_HEADER_ORDER
这种信号可以在不分析请求内容的情况下,立即识别出 curl、requests 和 axios。tls-client 通过 client_identifier 自动解决了这个问题:它会为所选浏览器应用正确的 H2 SETTINGS profile。这就是为什么参数 client_identifier="chrome_146" 不只是一个“Chrome 版本”,而是整个网络栈的完整 profile。