User-Agent 与 Client Hints:保持身份一致
User-Agent 请求头、Client Hints 和 navigator.userAgentData 为何必须一致,框架层覆盖为何留有缺口,以及如何设置并核对自定义身份。
BotBrowser Team
自定义 User-Agent 只有在请求头、Client Hints、navigator.userAgentData 和 Worker 作用域描述同一个浏览器时才可信。BotBrowser 可以用一个 --user-agent 模板加配套的 --bot-* 参数生成这些层面,但不能替你选出合理的取值,也不能决定站点如何对待该会话。
要点摘要
通过 args 中的 --user-agent 并为每个占位符配齐 --bot-* 参数来设置,不要用只改变单一层面的框架 userAgent 选项。然后在页面、Worker 和请求头中分别读取身份,确认三处一致。
本文目录
- 为什么 User-Agent 请求头、Client Hints 和 userAgentData 必须一致
- 必须保持一致的几个层面
- 为什么框架层或 CDP 的覆盖会留下缺口
- 在 BotBrowser 中设置自定义 User-Agent
- 在页面、Worker 和请求中核对身份
- 选择取值并按局限做规划
为什么 User-Agent 请求头、Client Hints 和 userAgentData 必须一致
User-Agent 请求头是浏览器向网站描述自己的最古老方式。多年来,它是一长串字符串,包含浏览器名称、完整版本、操作系统和渲染引擎。此后 Chrome 减少了这串字符串携带的细节,结构化的信息转移到了 User-Agent Client Hints,通常简称 UA-CH。结果是,同一个浏览器身份现在同时表现在多个位置:User-Agent 请求头、Sec-CH-UA 系列请求头、navigator.userAgent 字符串,以及 navigator.userAgentData API。
因此,自定义身份的可靠程度取决于它最薄弱的那一处。如果只改字符串,请求头说一套,Sec-CH-UA 说一套,navigator.userAgentData 又说一套。每处单独看都合法,合在一起却描述了一个并不存在的浏览器。BotBrowser 可以在受控上下文中让同一份配置的 User-Agent 字符串、Client Hints 和 navigator 值保持一致,但不能改变服务器如何对待这个身份。
这对隐私和日常可用性都有影响。User-Agent 请求头和低熵提示随第一个请求一起到达,早于任何脚本运行,所以服务器在页面加载之前就能看到它们。页面和分析脚本随后还可以通过 JavaScript 再读取同样的信息并加以比较。当答案不一致时,这种组合本身就不寻常,会让会话更容易被区分出来。它还会造成普通的故障,例如服务器根据 User-Agent 请求头选择了移动端布局,而脚本从 navigator.userAgentData 读到的却是桌面平台。
有三类读者最常遇到这个问题。为 Android 或 Edge 访客测试网站的团队,需要浏览器在所有位置都以这类访客的身份出现。为隐私而维护多个相互独立的浏览身份的人,需要每个身份保持稳定并且自身一致。复现客户问题的支持工程师,需要复现环境带有与客户相同的身份。三者依赖的都是各层面之间的一致,而不是某一个孤立的值。
在改动任何内容之前,先问自己三个问题:
- 哪些层面会报告这个身份,它们是否会一起变化?
- 所有版本号是否属于同一个 Chromium 主版本?
- 其余配置,例如区域设置、时区和网络路由,是否仍然描述同一个连贯的浏览器?
必须保持一致的几个层面
基于 Chromium 的浏览器通过四组值来描述自己。下面逐一说明,之后给出一份可以核对的一致性清单。
User-Agent 字符串
User-Agent 请求头和 navigator.userAgent 携带同一个字符串。桌面版 Chrome 上它形如:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.60 Safari/537.36
在 Chrome 的 User-Agent 精简方案下,这个字符串中的操作系统和版本部分保持为通用值,版本的次要部分也被固定。该字符串仍随每个请求发送,仍是大多数服务器最先读取的内容,所以它必须与取代其已省略细节的结构化值保持同步。 关于相关标准背景,以及从解析 User-Agent 逐步迁移到 Client Hints 的做法,可参阅 用户代理精简与客户端提示迁移实践。
Client Hints 请求头
Client Hints 以结构化形式给出同样的事实。低熵提示默认随请求发送:
Sec-CH-UA列出带主版本号的品牌标记,例如"Chromium";v="142", "Google Chrome";v="142", "Not:A-Brand";v="99"。Sec-CH-UA-Mobile表明浏览器是否为移动端,例如?0。Sec-CH-UA-Platform给出操作系统名称,例如"Windows"。
高熵提示只有在服务器通过 Accept-CH 响应头请求后才会发送:
Sec-CH-UA-Full-Version-List携带每个品牌的完整版本。Sec-CH-UA-Platform-Version携带操作系统版本。Sec-CH-UA-Arch和Sec-CH-UA-Bitness携带 CPU 架构和位数。Sec-CH-UA-Model携带设备型号,主要在移动端有意义。
由于详细提示是按需发送的,其中的不一致可能一直不被察觉,直到某个网站提出请求。首次加载时看起来正确的配置,在第二次加载时仍可能自相矛盾。 关于这些请求头如何被使用、又在哪里出现偏差,可参阅 Client Hints 指纹识别:HTTP 头部与身份暴露,该文对同一会话中的请求头取值与 JavaScript 取值做了对比。
navigator.userAgentData 与 getHighEntropyValues
同一信息在 JavaScript 一侧的形式是 navigator.userAgentData。它的 brands、mobile 和 platform 属性与低熵请求头对应。详细值来自 getHighEntropyValues(),它返回一个 Promise:
navigator.userAgentData.brands;
navigator.userAgentData.mobile;
navigator.userAgentData.platform;
await navigator.userAgentData.getHighEntropyValues([
'platformVersion',
'architecture',
'bitness',
'fullVersionList',
'model',
]);
请求头怎么说,这些属性也应该怎么说。规范把 API 和请求头名称一并定义,正因如此,两者应当被当作同一份描述来读。
Worker 作用域
专用 Worker、共享 Worker 和 Service Worker 各自拥有独立的 navigator 对象。如果某个 Worker 报告的 User-Agent 或 userAgentData 与启动它的页面不同,这就是明显的不一致,因为 Worker 属于同一个浏览器。任何只在页面上修改取值、不覆盖 Worker 作用域的做法,都会留下这个缺口。
GREASE 条目与版本对齐
Chromium 会在品牌列表中加入一个刻意无意义的 GREASE 条目,例如 "Not:A-Brand";v="99"。它的文本、版本和位置会随浏览器版本变化。其目的是防止服务器依赖固定的列表格式,因此手写的列表如果这个条目写错或位置不对,就会与真实列表不同。
版本号同样需要仔细对待。User-Agent 字符串中的主版本、Sec-CH-UA 中的版本以及 fullVersionList 中的条目,应当描述同一个 Chromium 发行版本。一处是完整版本 142.0.7444.60,另一处主版本却是 140,即使所有品牌名称都正确,这也是不一致。
可以核对的一致性清单
综合起来,一个可信的身份满足以下清单:
User-Agent请求头与navigator.userAgent一致。Sec-CH-UA中的品牌标记与navigator.userAgentData.brands一致。Sec-CH-UA-Platform中的平台与navigator.userAgentData.platform一致。- User-Agent 字符串中的主版本与
Sec-CH-UA品牌标记中的版本一致。 Sec-CH-UA-Full-Version-List中的完整版本与getHighEntropyValues()返回的fullVersionList一致。- 平台版本是所声明操作系统的真实取值。
- 架构和位数与平台相符,例如 Windows 桌面为
x86和64。 - 每个值在主线程、Worker 和 HTTP 请求头中都相同。
为什么框架层或 CDP 的覆盖会留下缺口
自动化框架让修改字符串变得很容易。Playwright 在创建上下文时接受 userAgent 选项,Puppeteer 提供 page.setUserAgent():
const context = await browser.newContext({ userAgent: 'Custom UA String' });
await page.setUserAgent('Custom UA String');
只改字符串的覆盖一定会修改 User-Agent 请求头和 navigator.userAgent,其余层面会怎样取决于具体调用。2026-10-23 用 Playwright 1.58.0 与 Puppeteer 24.40.0 实测:
- Playwright 的
browser.newContext({ userAgent })还会推导平台元数据:页面与 Worker 随后报告新平台,而品牌与版本仍可能描述底层 Chromium。 - Puppeteer 的
page.setUserAgent('Custom UA String')会让navigator.userAgentData的品牌和结构化值为空,而不是保留原值。
所以既不要假设「原始身份会保留」,也不要假设「整份身份会跟着字符串走」。在你实际使用的构建上读页面、Worker 和请求头;上面那份四表面记录才能告诉你属于哪种情况。
Chrome DevTools Protocol 提供带可选 userAgentMetadata 对象的 Network.setUserAgentOverride。它更完整,因为可以携带品牌、平台和其他提示值,但工作仍然落到你身上。品牌列表、GREASE 条目、完整版本和平台字段都要手写,并与字符串保持同步。覆盖只作用于它发送到的目标,所以每个页面和每个 Worker 都需要各自的调用,而对某个 Chromium 版本正确的列表,到下一个版本就过时了。
设置额外请求头也有同样的问题。在网络上改写 Sec-CH-UA 的规则会改变请求,却不会改变页面内 navigator.userAgentData 的返回值。于是身份在网络层和脚本层之间出现差异。如何在 BotBrowser 中通过参数、配置文件或 CDP 设置额外请求头,可参阅BotBrowser 自定义 HTTP 请求头:参数、配置文件或 CDP。
这些是逐个层面改写这类方法的一般性局限。请求头规则、字符串覆盖和修改单个属性的脚本,各自只覆盖自己的层面,没有任何东西保证改动之后这些层面仍然一致。可靠的做法是只选择一次身份,让每个层面都由这次选择推导而来。
在 BotBrowser 中设置自定义 User-Agent
BotBrowser 配置文件本身已经提供了连贯的基线。它包含相互匹配的 User-Agent 字符串、Client Hints 值和 userAgentData 字段,所以加载配置文件就能得到一致的身份,无需额外参数。只有当你想要与配置文件不同的身份时,才需要下面这些参数,例如在桌面电脑上使用 Android 身份。
--user-agent 参数接收原始字符串,并与一组描述结构化部分的 --bot-* 参数配合使用。自定义 User-Agent 和完整的 userAgentData 控制需要 ENT Tier3 许可证:
| 参数 | 控制内容 | 示例 |
|---|---|---|
--user-agent | 原始 User-Agent 字符串 | Mozilla/5.0 (Linux; ...) |
--bot-platform | userAgentData 中的平台 | Windows、Android |
--bot-platform-version | 操作系统版本 | 13、10.0 |
--bot-model | 设备型号,主要用于移动端 | SM-G991B |
--bot-architecture | CPU 架构 | x86、arm |
--bot-bitness | 系统位数 | 32、64 |
--bot-mobile | userAgentData 中的移动端标志 | true、false |
--user-agent 的值可以包含占位符,在浏览器启动时被替换。{platform}、{platform-version}、{model}、{architecture} 和 {bitness} 从对应的 --bot-* 参数取值,{ua-full-version}、{ua-major-version} 和 {brand-full-version} 则从版本参数取值。使用占位符能让字符串和结构化值绑定到同一个来源,这样就不会出现只改一处而忘了另一处的情况。
两个辅助参数用于设置版本。--bot-ua-full-version(ENT Tier2)设置 Chromium 完整版本,--bot-brand-full-version(ENT Tier2)为 Edge 等品牌设置品牌自身的版本。文档建议让 --bot-ua-full-version 与你所用 BotBrowser 二进制文件的 Chromium 主版本保持一致。 关于如何在 Chrome、Edge 和 Brave 之间选择并核对品牌本身,可参阅 浏览器品牌切换:Chrome、Edge、Brave、Opera,该文介绍了在启动时设置品牌的方法。
桌面电脑上的 Android 身份如下所示:
chrome --bot-profile="path/to/android-profile.enc" \
--user-agent="Mozilla/5.0 (Linux; Android {platform-version}; {model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ua-full-version} Mobile Safari/537.36" \
--bot-platform=Android \
--bot-platform-version=13 \
--bot-model=SM-G991B \
--bot-mobile=true \
--bot-ua-full-version=142.0.7444.60
BotBrowser 会用参数值替换 {platform-version}、{model} 和 {ua-full-version},然后生成与之匹配的 navigator.userAgentData、带有正确 GREASE 条目的品牌列表、高熵值以及 Client Hints 请求头。根据 BotBrowser 文档,结果在主线程、Worker 和 HTTP 请求中保持一致。
Windows 桌面身份遵循同样的模式,只是取值不同:
chrome --bot-profile="path/to/windows-profile.enc" \
--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ua-full-version} Safari/537.36" \
--bot-platform=Windows \
--bot-platform-version=10.0 \
--bot-architecture=x86 \
--bot-bitness=64 \
--bot-mobile=false \
--bot-ua-full-version=142.0.7444.60
Playwright 和 Puppeteer 通过各自的 args 选项传入同样的参数,而不是通过框架层的 userAgent 设置。请像文档示例那样,通过 args 传入这些参数。若要切换品牌本身,这是另一项 ENT Tier2 控制,请参阅文末链接的品牌文章。
在页面、Worker 和请求中核对身份
启动之后,无需额外工具,就可以在浏览器内核对结果。目标是在网站可能读取的三个位置读到同一个身份。
先从页面开始。在任意页面打开开发者工具,在控制台运行下面几行:
navigator.userAgent;
navigator.userAgentData.brands;
navigator.userAgentData.platform;
await navigator.userAgentData.getHighEntropyValues([
'platformVersion',
'architecture',
'bitness',
'fullVersionList',
'model',
]);
然后检查请求。打开“网络”面板,选择一个顶层 HTTPS 请求,查看其请求头。User-Agent、Sec-CH-UA、Sec-CH-UA-Mobile 和 Sec-CH-UA-Platform 请求头应当与控制台输出一致。详细请求头,例如 Sec-CH-UA-Full-Version-List,只有在网站通过 Accept-CH 请求后才会出现,所以第一个请求中没有它们是正常的,不属于不一致。
最后检查 Worker 作用域。下面这段简短的代码从 blob 启动一个专用 Worker,并输出 Worker 报告的内容:
const worker = new Worker(
URL.createObjectURL(new Blob(['postMessage([navigator.userAgent, navigator.userAgentData.platform])']))
);
worker.onmessage = event => console.log(event.data);
Worker 输出的两个值应当与页面一致。如果你的页面使用 Service Worker,请在“应用”面板中检查它,并在其控制台运行同样的两个表达式。
当某处看起来不对时,症状通常能指向原因:
| 症状 | 可能原因 | 如何调整 |
|---|---|---|
字符串中原样出现 {model} 之类的占位符 | 缺少对应的 --bot-* 参数 | 设置 --bot-model,或移除该占位符 |
请求头显示 Android,但 userAgentData.platform 显示 Windows | 没有设置 --bot-platform | 添加 --bot-platform=Android 及其他平台参数 |
Sec-CH-UA 中的版本与 User-Agent 字符串不同 | 版本参数与 Chromium 主版本不一致 | 让 --bot-ua-full-version 与二进制文件的 Chromium 版本一致 |
| 字符串改了,但提示没有跟着改变 | 字符串是通过框架选项设置的 | 在 args 中用 --user-agent 传入 |
| Worker 报告的值与页面不同 | 页面级脚本或覆盖只改动了页面 | 移除页面级覆盖,保留浏览器参数 |
每次更改配置文件、Chromium 版本或身份参数之后都要重复核对,因为其中任何一项都可能只移动一个层面而不移动其他层面。核对通过只说明浏览器自身是一致的,并不说明某个具体网站会如何对待该会话,因为网站会综合多种输入,并随时间调整自己的规则。
选择取值并按局限做规划
这些参数让各层面彼此一致,但不会替你挑选合理的取值,所以结果的质量取决于你填入的内容:
- 使用真实且当前的版本。几乎没人还在使用的浏览器版本本身就不寻常,因此请跟随二进制文件的 Chromium 主版本。
- 选择彼此匹配的值。平台、平台版本、型号、架构和位数应当描述同一种真实设备。
- 让周边设置保持一致。身份、时区、区域设置、语言和网络路由应当描述同一类访客。
- 在会话或账号的整个生命周期内保持同一个身份。在两次访问同一网站之间更改字符串,会留下不一致的历史。
- 浏览器或配置文件更新后,重新运行核对。
并不总是需要这些参数。加载的配置文件本身就是一致的,所以不设置 --user-agent 是保持一致最简单的办法。只有当你有理由呈现另一个身份时才使用自定义身份,例如在桌面电脑上复现仅在 Android 上出现的支持案例,然后确认每个层面。每多一个覆盖,就多一个必须与其余部分保持同步的值,所以只改任务所需要的内容。
几个常见的实际问题:
- 只设置
--user-agent而不设置对应的--bot-*参数会怎样? 为所用的每个占位符设置对应的--bot-*参数;例如,{model}需要--bot-model。文档建议将占位符语法与对应参数配合使用。 - 身份会作用到 Worker 吗? 会。根据文档,取值在主线程、Worker 和 HTTP 请求中保持一致,上面的核对步骤可以让你自行确认。
- 只使用配置文件的设置需要许可证吗? 不涉及自定义参数,使用的是配置文件中采集的值。自定义 User-Agent 和完整的
userAgentData控制需要 ENT Tier3 许可证。 - 自定义身份会改变浏览器的功能吗? 这些参数改变的是所报告的身份值。它们不会添加底层浏览器本来没有的功能,所以改动之后请测试你关心的页面。
BotBrowser 支持带占位符的自定义 --user-agent 模板,并配合 --bot-platform、--bot-platform-version、--bot-model、--bot-architecture、--bot-bitness 和 --bot-mobile,生成相互匹配的 Client Hints 品牌、高熵值和 HTTP 请求头,使其在主线程、Worker 和请求中保持一致。这样,上文所述的手工对账就被换成了只需验证一个身份,而不是多个。BotBrowser 不能让网站接受所选择的身份,不能替代对真实且当前版本值的选择,并且带完整 userAgentData 控制的自定义 User-Agent 需要 ENT Tier3 许可证。
若要切换浏览器品牌,请参阅Chrome、Edge 与 Brave 的浏览器品牌不一致问题。若想了解网站如何使用 Client Hints,请参阅Client Hints 指纹识别:HTTP 头部与身份暴露。若要把身份与地理设置结合起来,请参阅时区、区域设置和语言指纹指南。
相对参考文档新增的内容
参考页列出 User-Agent 选项和配套参数,但没有给你一份可以跨两次启动做对比的记录。缺少这份记录时,不一致很容易被误读,因为框架覆盖会掩盖究竟是哪个作用面发生了变化。
| 作用面 | 在哪里读取 | 不一致说明什么 |
|---|---|---|
| 页面 | 页面的控制台 | 覆盖生效了,但给出的值不完整 |
| Worker | Worker 的控制台 | 只改了页面,没有影响 Worker 作用域 |
| 请求 | 网络面板中的请求 | 只在连接层改写,页面看不到 |
| Hints | 在页面里调用 getHighEntropyValues() | 权限策略或某处覆盖让高熵值集合不完整 |
在修改配置文件之前,先把四份读数保存下来。先在页面执行下面三行,再在 Worker 里执行同样的两行,然后打开网络面板,把请求头复制到同一个文件里。每次运行保留一份文件,按 mode 命名,对比这两份文件,而不是凭记忆重跑检查。
// 页面和 Worker 都接受这种结构
const identity = {
ua: navigator.userAgent,
brands: navigator.userAgentData?.brands?.map(b => `${b.brand}/${b.version}`),
mobile: navigator.userAgentData?.mobile,
platform: navigator.userAgentData?.platform,
};
identity.high = await navigator.userAgentData.getHighEntropyValues([
'platformVersion',
'architecture',
'bitness',
'fullVersionList',
]);
console.log(JSON.stringify(identity));
如果页面记录和 Worker 记录不一样,说明页面之下有东西在改写身份。只给主线程打补丁的框架是最常见的原因;在你测试的这版构建里,先停掉这个覆盖并重做两次读取来确认,因为其他机制也能造成同样的分裂。覆盖还在时,调参数补不上这个缺口。如果请求头和页面记录不一样,说明改动只发生在连接层。如果 high 是空的,先看该页面的 ch-ua-high-entropy-values 权限策略:只有策略允许时,getHighEntropyValues() 才会返回值。一个从未向服务器请求详细提示的页面也可能照常返回值,而发过 Accept-CH 的页面也可能拒绝这个 API,所以要分别检查 API 和请求头,而不是断定配置坏了。
来源
- BotBrowser 文档:Custom User-Agent
- Chrome for Developers:User-Agent 精简
- WICG:User-Agent Client Hints 规范
- MDN:NavigatorUAData
- RFC 8942:HTTP Client Hints
实用结论
当已加载的配置文件已经描述了你想要的身份时,不要设置 --user-agent;只有在有明确理由时才使用自定义身份,例如在桌面机上复现仅限 Android 的问题。使用时,版本参数要对齐二进制的 Chromium 主版本,取值要描述同一种真实设备。每次更改配置文件或 Chromium 版本后重做页面、Worker 和请求的核对;通过只说明内部一致,不说明站点会如何响应。
本文属于指南: 浏览器身份表面:一致性实用总览