改 user-agent 几乎骗不了人。了解网站如何借助请求头顺序、Client Hints、JS 引擎探测和指纹矛盾识破伪装。
user-agent 伪装,是指修改浏览器发送的 user-agent 字符串,让网站以为你在使用另一种浏览器、设备或操作系统。这件事很容易做到——一个设置或一个扩展就能搞定——但面对真正在留意的网站,它几乎从不奏效,因为 user-agent 只是浏览器发出的数十种信号之一,其余信号会悄悄拆穿这个谎言。本文将讲解伪装是如何被检测出来的:请求头一致性、Client Hints、JavaScript 引擎探测,以及指纹矛盾。
核心要点
- user-agent 字符串只是一个自报的、可随意修改的声明——单凭它服务器什么也核实不了,因此现代检测把它当作一个需要交叉核对的未经核实的断言。
- 浏览器还会泄露数十种其他信号(HTTP 请求头顺序、
sec-*请求头、User-Agent Client Hints、JavaScript 引擎行为、canvas/WebGL/屏幕/触摸),它们全都必须与所声称的身份相符。 - 伪装之所以失败,是因为伪造一行文字轻而易举,而始终一致地伪造一个浏览器全部可观测的行为却极其困难。
- 检测是概率性的:每一处矛盾都会抬高怀疑分值,而不是给出一个非黑即白的判定。
- 引擎层面的行为和 HTTP 请求头一致性是最难伪造的几项信号,因为它们要求复刻一个真实浏览器的完整实现。
什么是 user-agent 伪装?
你的浏览器发出的每一个请求都包含一段 user-agent 字符串——一行类似 Mozilla/5.0 ... Chrome/120.0 ... 的文字,用来声明浏览器、版本和平台。伪装就是刻意修改这段字符串。人们这么做的理由很多:隐私(融入人群或破坏追踪)、访问限定特定浏览器的内容、网页抓取、自动化测试,或者——在恶意的一端——伪装成真实用户的欺诈和机器人流量。
修改字符串本身轻而易举。问题在于,user-agent 只是一个声明,而浏览器还会泄露关于自身的数十项其他事实,服务器可以拿这些事实去与该声明交叉核对。一旦对不上,伪装就暴露了。
为什么单凭 user-agent 字符串不可信
只读取 user-agent 的服务器,等于全盘相信浏览器对自己的描述。但 user-agent 是自报的、可随意修改的,所以单凭它什么也证明不了。现代检测会把它当作一个未经核实的断言,然后追问:这个浏览器做的其余一切,是否真的与它所声称的那个浏览器的真实实例相符?通常并不相符,因为伪造字符串很容易,而伪造一个浏览器全部可观测的行为却极其困难。
网站如何检测被伪装的 user-agent
检测的原理是收集相互独立的信号,再核查它们内部是否一致。主要技术如下:
HTTP 请求头一致性(顺序与 sec-* 请求头)
不同浏览器发送的 HTTP 请求头各不相同,顺序不同,取值也不同。Chrome 会发送一组 Firefox 没有的 sec-ch-ua 和 sec-fetch-* 请求头;User-Agent 请求头在请求中的位置也因浏览器而异;而 Accept、Accept-Language 和 Accept-Encoding 的取值则遵循各浏览器特有的模式。如果一个请求声称是 Chrome,却缺少 Chrome 的 sec-* 请求头——或者像 Firefox 那样排列请求头——那这个声明就值得怀疑。这些细节都不会体现在 user-agent 字符串本身里,而大多数伪装工具也懒得去复刻它们。
Client Hints 与 user-agent 的矛盾
Chromium 系浏览器会暴露 User-Agent Client Hints,这是一个结构化、可由 JavaScript 读取的来源,通过 navigator.userAgentData API 提供同样的浏览器/平台信息。服务器可以请求高熵 Client Hints,再拿它们与 user-agent 字符串作比对。如果 user-agent 说自己是「macOS 上的 Safari」,但 Client Hints API 却存在、并报告自己是 Windows 上的 Chromium,二者就互相矛盾,伪装一目了然——因为真正的 Safari 根本不会暴露 Chromium 的 Client Hints。
JavaScript 引擎与功能探测
每种引擎实现 JavaScript 和网络平台的方式都略有差异,而这些差异在代码中是可观测的。检测器可以检查所声称的浏览器本应具备的 API 是否存在,测试引擎特有的怪癖,或运行微基准测试,用其耗时来给 JavaScript 引擎打指纹。一个暴露了 Chrome 专属 API 的「Firefox」,或者引擎行为与 V8 而非 SpiderMonkey 相符的「Firefox」,就是被伪装的。这直接关联到浏览器真正使用的渲染引擎——引擎远比名字难以伪造。
指纹矛盾(canvas、WebGL、屏幕、触摸)
最后,设备本身也会泄露一些本应与所声称的浏览器和平台相符的信号:
- canvas 和 WebGL 的渲染会因 GPU、驱动和操作系统而异;一个移动端 user-agent 背后却是桌面 GPU 的签名,就是危险信号。这一机制的原理见 canvas 指纹检测。
- 屏幕尺寸若大于可见窗口,或与所声称的设备类别不符,都很可疑。
- 触摸支持:手机 user-agent 出现在一台报告不存在触摸事件的设备上,很难说得通。
单个信号本身都很弱,但组合起来就构成一份 user-agent 必须匹配的画像——而简单地改一行字符串是跟不上这份画像的。
服务端与客户端信号如何拆穿被伪装的 UA
伪装之所以站不住脚,是因为它必须同时骗过两个截然不同的观察者:读取原始 HTTP 的服务器,以及在页面内运行的 JavaScript。一个修补了其中一方的伪装工具,往往忘了另一方,于是两边的说法便不再一致。
在服务端,在任何脚本运行之前,请求本身就已经带着破绽。HTTP/2 请求头的组合与顺序、是否存在 Chromium 专属的 sec-ch-ua 请求头,以及 TLS ClientHello,都在页面被解析之前就已被观测到。尤其是 TLS 握手会产生一个稳定的签名——通常概括为 JA3 或 JA4 哈希——它源自客户端所提供的密码套件与扩展。这个签名反映的是底层网络栈(Chromium 的 BoringSSL、Firefox 的 NSS),而非 user-agent 字符串,所以一个声称是 Firefox、却呈现 Chromium TLS 指纹的请求,本身就自相矛盾。这一切都无法通过在浏览器设置里修改 user-agent 来改变。
在客户端,JavaScript 可以直接盘问运行时。navigator.userAgentData 要么存在(Chromium),要么不存在(Firefox、Safari)——一段被伪装的字符串无法凭空把这个 API 变出来。除此之外,引擎专属全局对象与 API 的有无、数字和日期的精确序列化方式、错误消息的措辞,以及紧凑循环的耗时特征,全都会暴露真正在执行的是哪种 JavaScript 引擎。就连 CSS 也会出卖引擎:带厂商前缀的属性、表单控件的默认渲染,以及通过 CSS.supports() 查询的支持标志,在 Blink、Gecko 与 WebKit 之间各不相同。检测器要做的,不过是看这些客户端事实是否与请求头和 user-agent 的声称对得上。
声称 vs 实际:几个有代表性的例子
| 所声称的身份 | 真实实例本应呈现 | 粗糙伪装常常呈现 |
|---|---|---|
| macOS 上的 Safari | 没有 sec-ch-ua 请求头;navigator.userAgentData 未定义;WebKit 的 CSS 破绽 | Chromium 的 sec-ch-ua 请求头与已定义的 userAgentData |
| Windows 上的 Firefox | Gecko 的请求头顺序;SpiderMonkey 的耗时;没有 Chromium Client Hints | V8 风格的引擎行为与 Chromium 的 TLS/JA3 签名 |
| iPhone Safari(移动端) | 存在触摸事件;WebGL 报告移动端 GPU;屏幕较小 | 桌面端 GPU 渲染器字符串且不支持触摸 |
| Android 上的 Chrome | sec-ch-ua-mobile: ?1;ARM GPU 字符串 | sec-ch-ua-mobile: ?0 或桌面端 NVIDIA/Intel GPU 字符串 |
每一行都是检测器可以标记的矛盾,而它无需「证明」user-agent 为假——只要注意到两个相互独立的信号不可能同时为真即可。
如何自己动手测试
你不需要一个服务器集群就能看到这一点。打开浏览器的 DevTools 控制台,并排运行 navigator.userAgent 和 navigator.userAgentData:在 Chromium 上后者返回一个对象,在 Firefox 和 Safari 上则是 undefined,无论你伪装成什么字符串都一样。用网络面板查看浏览器实际发送的请求头,确认 sec-ch-ua 是否存在。然后用扩展或 DevTools 的设备模拟修改你的 user-agent 再重复一遍——你会看到字符串变了,而 Client Hints、引擎行为和 GPU 字符串还和原来一模一样。这道落差,正是检测系统所衡量的东西。
检查你自己的浏览器是否存在矛盾
你可以在自己身上看到许多这类信号。运行 BrowserInsight 的 浏览器内核检测,把你声称的浏览器与它真实的渲染引擎作对比,再用 机器人检测工具 查看自动化系统会权衡的那些一致性信号。如果你使用了会改动 user-agent 的隐私工具,这也是判断它们是否引入了新的矛盾、从而让你更易被识别而非更难的方法。
常见问题
修改 user-agent 违法吗?
不违法。修改 user-agent 是浏览器和扩展允许的正常、合法操作,它有测试和兼容等正当用途。法律问题只来自你拿它做什么——欺诈或未授权访问——而非修改字符串这件事本身。
网站能 100% 确定地检测出伪装吗?
不一定能百分百确定,但往往能高度确信。检测是概率性的:每一处矛盾(缺失的请求头、对不上的 Client Hints、错误的引擎行为、冲突的指纹)都会抬高怀疑分值。一个让所有信号都保持一致的精心伪装确实难以抓住,但普通的 user-agent 修改会留下许多破绽。
隐私工具和反指纹扩展会触发误报吗?
有时会。会修改 user-agent 或随机化信号的工具,可能制造出被检测系统读作伪装的矛盾,这也是注重隐私的用户偶尔会遭遇额外机器人挑战的原因之一。设计良好的工具会一致地修改信号以避免这一点;粗糙的工具反而让你更显眼。
检测伪装最可靠的信号是什么?
没有单一最可靠的信号——可靠性来自把它们组合起来。话虽如此,引擎层面的行为和 HTTP 请求头一致性是最难伪造的几项之一,因为它们要求复刻一个真实浏览器的完整实现,而不只是修改一行文字。
结语
user-agent 伪装易于尝试却难以成功,因为浏览器透露的关于自身的信息,远多于它主动声明的那段字符串。请求头顺序与 sec-* 请求头、Client Hints、JavaScript 引擎行为以及设备指纹,全都必须与那个声明相符——而一行修改无法让它们保持一致。无论你是在防护一个网站,还是在审视自己的隐私配置,结论都一样:永远不要单凭 user-agent,而要核查它周围的信号。
推荐阅读: