真正让我下决心去研究 tls-client 的,是一句反直觉的排查结论:“你的请求头没问题,但你 ClientHello 一眼就是 Python 发的。”当时我拿 requests 把 User-Agent、Accept、Accept-Language 甚至 Sec-Fetch 全按 Chrome 的真实请求补了一遍,结果对面的开放平台接口依然把我当异常流量处理。TLS指纹,这个以前只在逆向报告里瞄过一眼的词,成了那周我查资料的唯一关键词。
tls-client 这类库解决的核心问题,就是让一个非浏览器客户端在“网络握手层”长得像浏览器。它不是简单改几个 Header,而是直接控制 TLS 客户端握手包的构造方式,从而改变服务端计算出的 TLS 指纹。这篇东西我打算把它从原理讲到能直接抄作业的实战用法:先解释指纹到底是从哪来的,再说 tls-client 背后的 uTLS 机制,然后给完整代码示例,最后把我踩过的坑和排查思路一并倒出来。适合所有写爬虫、做开放平台联调、搞自动化测试时被“莫名风控”卡住的人。
1. 被识别的是握手包里的“体貌特征”,不是你的 User-Agent
1.1 你的请求头再“像浏览器”,底层套件也藏不住
很多人对“浏览器指纹”的第一反应是 UA、Canvas、WebGL 那些东西,但网络请求层面的指纹完全不同。当你用 HTTPS 访问一个站点时,浏览器并不是上来就传数据,而是先做 TLS 握手。握手的第一步叫 ClientHello,客户端会在这一步告诉服务器:我支持哪些 TLS 版本、我支持哪些加密套件、我支持哪些扩展,以及这些扩展以什么顺序排列。
服务器看到 ClientHello 后,就能基于这些字段的组合算出一个“握手指纹”。比较知名的算法是 JA3:把 TLS 版本、密码套件列表、扩展列表、椭圆曲线列表、椭圆曲线点格式这几个字段按固定规则拼成字符串,再做一次 MD5,得到一个 32 位的指纹值。同一个浏览器、同一个 OpenSSL 版本、同一个底层的 TLS 库,算出来的 JA3 基本是稳定的。
问题在于,Python 的 requests 底层用的是 Python 自带的 ssl 模块,而 ssl 模块最终调用的又是 OpenSSL。OpenSSL 默认构造 ClientHello 的方式和 Chrome 用的 BoringSSL 完全不是一路人。Chrome 的 ClientHello 里有一堆占位用的 GREASE 扩展,OpenSSL 没有;Chrome 的加密套件顺序是 TLS 1.3 套件在最前,OpenSSL 的顺序往往受系统编译参数影响;Chrome 还会带 application_settings 这类扩展用于 ECH,而 OpenSSL 通常不带。于是哪怕你在应用层把 headers 伪装得跟真浏览器一模一样,服务端只要算一下 JA3,就知道对面不是浏览器。
这也是为什么很多反爬风控系统能轻易识别 requests、httpx、curl 的原因:不是因为它们不守规矩,而是因为它们在网络层的“指纹长相”太有辨识度了。
1.2 JA3/JA4 与 HTTP/2 指纹:两项关键检测指标
JA3 在很长一段时间里是 TLS 指纹检测的主流方案,但它有个明显缺点:只看字段列表,不看字段内部的数据和顺序细节。举个极端例子,两个不同版本的 Chrome,如果它们支持的套件列表完全一致,JA3 就可能相同;但它们的扩展内部细节可能差异很大。于是后来出现了 JA4。
JA4 的算法比 JA3 复杂不少,会把 TLS 版本、SNI 类型、证书压缩方式、扩展数量、签名算法、ALPN 协议等多个维度组合编码,甚至区分了客户端和服务端的指纹。它的指纹格式是一串形如 t13d2317... 的字符串,前半段标识 TLS 版本和握手类型,后面才是一长串具体特征。JA4 让“指纹伪装”的难度更高,单纯复制某个 JA3 字符串已经不够,必须在扩展级层面保持一致性。
比 TLS 指纹更晚进入公众视野的,是 HTTP/2 指纹。现在主流浏览器默认用 HTTP/2 通信,而 HTTP/2 连接建立之初,客户端会发一个 SETTINGS 帧,告诉服务器自己的一些参数,比如最大并发流数量、初始窗口大小、是否启用推送等。不同浏览器对 SETTINGS 的参数值、参数顺序、是否发送 WINDOW_UPDATE、优先级帧的处理方式都不太一样。把这些行为组合起来,就形成了类似 Akamai HTTP/2 Fingerprint 的检测结果。
如果把 TLS 指纹类比成一个人的脸型轮廓,HTTP/2 指纹就是走路的姿态。脸可以整,但步态很难完全模仿。JA3/JA4 和 HTTP/2 指纹组合在一起,构成了现在服务端判断“对面是不是真浏览器”的两大底层依据。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. tls-client 的实现:uTLS 引擎如何伪造 ClientHello
2.1 tls-client 的本质:一个带浏览器指纹能力的 HTTP 客户端
我第一次接触 tls-client 时,直觉以为它只是把常见的 JA3 字符串预先存好,发请求时替换一下。真看了源码才发现不是这么简单。
tls-client 的底层是 Go 语言写的,核心依赖是一个叫 uTLS 的库。uTLS 比较特别的地方在于,它把 TLS 握手消息的构造权限彻底放开,允许你按任意顺序、任意内容拼一个 ClientHello 出来。普通 TLS 库要求你必须在一定的安全框架内选择套件,uTLS 则不限制这些,甚至可以主动生成 GREASE 值、修改扩展顺序、插入重复扩展。正是因为有这个能力,它的作者才能把 Chrome、Firefox、Safari 的真实 ClientHello 结构还原出来,封装成一个个 presets。
Python 版的 tls-client 是 Go 原版的一个绑定,通过 c-shared 把 Go 编译成动态库,再用 ctypes 从 Python
