HTTP/2 是如何做反爬的?
HTTP/2 是如何做反爬的? HTTP/2 反爬的原理是:客户端建立连接时,会按自己的实现发出一组底层参数和帧,这组特征相当稳定,能当作指纹识别客户端类型。服务器把这个指纹和请求头里声称的 User-Agent 做交叉验证。 一、HTTP/2 连接建立过程与指纹来源 客户端 → 服务器: 连接前缀 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n SETTINGS 帧 ← 指纹
HTTP/2 是如何做反爬的?
HTTP/2 反爬的原理是:客户端建立连接时,会按自己的实现发出一组底层参数和帧,这组特征相当稳定,能当作指纹识别客户端类型。服务器把这个指纹和请求头里声称的 User-Agent 做交叉验证。
一、HTTP/2 连接建立过程与指纹来源
客户端 → 服务器:
- 连接前缀
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n - SETTINGS 帧 ← 指纹来源 ①
- WINDOW_UPDATE 帧 ← 指纹来源 ②
- PRIORITY 帧(部分浏览器会发)← 指纹来源 ③
- HEADERS 帧(第一个请求)← 指纹来源 ④ 伪头部顺序、⑤ 普通头部顺序
RFC 只规定了这些帧"可以怎么发",具体发哪些参数、取什么值、按什么顺序,由各家实现自己决定,所以每种客户端都有自己的特征。
① SETTINGS 帧:参数、取值和顺序
| ID | 参数名 | 含义 |
|---|---|---|
| 1 | HEADER_TABLE_SIZE | HPACK 压缩表大小 |
| 2 | ENABLE_PUSH | 是否允许服务器推送 |
| 3 | MAX_CONCURRENT_STREAMS | 最大并发流数 |
| 4 | INITIAL_WINDOW_SIZE | 每个流的初始流控窗口 |
| 5 | MAX_FRAME_SIZE | 最大帧大小 |
| 6 | MAX_HEADER_LIST_SIZE | 最大头部列表大小 |
不同客户端的差别举例(具体数值随版本变化,下面只是示意):
- Chrome:发 1、2、4、6 四项,ENABLE_PUSH=0,INITIAL_WINDOW_SIZE 约 6MB
- Firefox:发 1、4、5,窗口约 128KB
- Go net/http、Python h2/httpx、curl:参数集合和取值各不相同
带哪些参数、参数顺序、具体取值,这三点组合起来已经能分辨大多数客户端。
② WINDOW_UPDATE 增量
连接级流控窗口一开始是 65535,客户端通常马上发一个 WINDOW_UPDATE 把它调大。增量值各家不同:
- Chrome 常见的是 15663105(加上初始值约 15MB)
- Firefox 是另一个值
- 很多库则根本不发
③ PRIORITY 帧和流优先级
- 旧版 Firefox 一连接就发若干 PRIORITY 帧,建一棵"空闲流依赖树",Chrome 不发。
- 请求 HEADERS 帧里的优先级字段(依赖哪个流、权重、是否独占)也各有习惯。
- 新版浏览器开始改用 RFC 9218 的 priority 请求头(例如
u=0, i),这个头有没有、值是多少,也是特征。
④ 伪头部顺序
HTTP/2 用 4 个伪头部表示请求行,RFC 没有规定顺序:
| 客户端 | 顺序 | 缩写 |
|---|---|---|
| Chrome | :method :authority :scheme :path |
m, a, s, p |
| Firefox | :method :path :authority :scheme |
m, p, a, s |
| Safari | :method :scheme :path :authority |
m, s, p, a |
这是区分度最高、最难伪造的特征之一。
⑤ 普通头部顺序和 HPACK 行为
-
sec-ch-ua、user-agent、accept、sec-fetch-*等头的排列顺序是固定的。 - 浏览器会把头名转成小写,并按固定策略决定哪些头放进 HPACK 动态表,哪些标记为"永不索引"(比如 Cookie)。这些编码细节也会被采集。
二、指纹怎么算:Akamai 格式
业界最常用的是 Akamai 2017 年公开的格式(BlackHat 论文 Passive Fingerprinting of HTTP/2 Clients):
SETTINGS | WINDOW_UPDATE | PRIORITY | 伪头部顺序
Chrome(示意):
1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
把这个字符串做哈希,就得到一个短 ID,和 TLS 的 JA3/JA4 指纹放在一起使用。Cloudflare、Akamai、阿里云、腾讯云等的 WAF 都会提取类似特征。
三、服务器怎么用这个指纹判断
单看一个指纹没有意义,关键是交叉验证是否一致。
和 UA 对比
UA 写的是 Chrome/131,HTTP/2 指纹却是 Python h2 的,或者是 Firefox 的 m,p,a,s,就说明在伪装。和 TLS 指纹对比
TLS 的 ClientHello 像 Chrome(JA4 吻合),HTTP/2 层却不像,说明用了只伪造 TLS 的工具,没伪造 HTTP/2。协议降级
现代 Chrome 访问支持 h2 的站点几乎一定走 HTTP/2。如果 UA 是 Chrome,ALPN 却只协商 http/1.1,或者根本没提供 h2,本身就是很强的异常信号。同一会话里前后不一致(这条和我们项目直接相关)
同一个 Cookie 或 token 会话里,页面 HTML、JS、图片都走 h2 并带 Chrome 指纹,只有joblist.json这一个接口突然走 HTTP/1.1、带 Node.js 的 TLS 指纹,后台很容易发现"这个 token 被转交给另一个程序在用"。行为层面
流 ID 递增方式、是否响应 PING、是否主动发 RST_STREAM、并发流的数量,也能用来辅助判断。
判定结果一般不会直接封禁,而是计入风险分,再结合 IP 信誉、请求频率、JS 环境检测等综合处理。轻的弹验证,重的返回错误码或假数据。BOSS 返回的 code=37 您的环境存在异常 就是风控拦截的一种表现,但目前没证据说明它依据的是哪个层面。
四、常见的应对方式
| 方式 | 原理 | 局限 |
|---|---|---|
| 直接用真实浏览器发请求 | TLS、HTTP/2、JS 环境全部真实 | 资源占用大,速度慢 |
| curl_cffi / curl-impersonate | 改过的 curl,按浏览器参数配置 TLS 和 HTTP/2 帧 | 要跟着浏览器版本更新,JS 生成的 token 还得另外解决 |
| tls-client(Go,基于 utls 加改造的 http2) | 同上,可选的浏览器模板多 | 同上 |
| 普通 requests / Node https | 只有 HTTP/1.1,TLS 是 OpenSSL 默认指纹 | 很容易被识别 |
五、回到我们项目
之前查过源码,route.fetch() 底层用的是 Node 的 http/https 模块(server/fetch.js:36-37),没有 http2。所以在 BOSS 的服务器看来:
- 页面请求:TLS 和 HTTP/2 都是 Chrome 的特征
-
joblist.json:Node.js 的 TLS 指纹,HTTP/1.1,没有 HTTP/2 指纹,但带着同一个
__zp_stoken__和 Chrome 的 UA
这正好是第三节第 4 条说的"同一会话前后不一致"。目前本地能通过,说明 BOSS 对这个接口暂时没有严格校验这一层,或者对家庭宽带 IP 比较宽松。这不能保证在云服务器 IP 上也能通过,也不能保证以后一直有效。
如果容器里出现之前列的那些失败症状,我建议改成上次说的方案 A:用 CDP 的 Fetch 接口在响应阶段暂停请求再读数据。这样请求始终由 Chrome 发出,TLS 和 HTTP/2 指纹都是真实的。
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.