返回知识中心
身份

User-Agent 与 Client Hints:保持身份一致

User-Agent 请求头、Client Hints 和 navigator.userAgentData 为何必须一致,框架层覆盖为何留有缺口,以及如何设置并核对自定义身份。

BotBrowser Team

文档中心

想直接看维护中的产品文档?

这篇文章对应的主题已经有文档中心页面。需要规范流程、当前参数和长期参考时,优先看 docs。

自定义 User-Agent 只有在请求头、Client Hints、navigator.userAgentData 和 Worker 作用域描述同一个浏览器时才可信。BotBrowser 可以用一个 --user-agent 模板加配套的 --bot-* 参数生成这些层面,但不能替你选出合理的取值,也不能决定站点如何对待该会话。

要点摘要

通过 args 中的 --user-agent 并为每个占位符配齐 --bot-* 参数来设置,不要用只改变单一层面的框架 userAgent 选项。然后在页面、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 读到的却是桌面平台。

示意图:一个已配置的身份决定 HTTP 请求头、主线程、Worker 作用域和高熵值,最后核对这四处是否一致

有三类读者最常遇到这个问题。为 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 取值做了对比。

同一信息在 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,即使所有品牌名称都正确,这也是不一致。

可以核对的一致性清单

综合起来,一个可信的身份满足以下清单:

  1. User-Agent 请求头与 navigator.userAgent 一致。
  2. Sec-CH-UA 中的品牌标记与 navigator.userAgentData.brands 一致。
  3. Sec-CH-UA-Platform 中的平台与 navigator.userAgentData.platform 一致。
  4. User-Agent 字符串中的主版本与 Sec-CH-UA 品牌标记中的版本一致。
  5. Sec-CH-UA-Full-Version-List 中的完整版本与 getHighEntropyValues() 返回的 fullVersionList 一致。
  6. 平台版本是所声明操作系统的真实取值。
  7. 架构和位数与平台相符,例如 Windows 桌面为 x86 和 64。
  8. 每个值在主线程、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-platformuserAgentData 中的平台Windows、Android
--bot-platform-version操作系统版本13、10.0
--bot-model设备型号,主要用于移动端SM-G991B
--bot-architectureCPU 架构x86、arm
--bot-bitness系统位数32、64
--bot-mobileuserAgentData 中的移动端标志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 选项和配套参数,但没有给你一份可以跨两次启动做对比的记录。缺少这份记录时,不一致很容易被误读,因为框架覆盖会掩盖究竟是哪个作用面发生了变化。

作用面在哪里读取不一致说明什么
页面页面的控制台覆盖生效了,但给出的值不完整
WorkerWorker 的控制台只改了页面,没有影响 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 和请求头,而不是断定配置坏了。

来源

实用结论

当已加载的配置文件已经描述了你想要的身份时,不要设置 --user-agent;只有在有明确理由时才使用自定义身份,例如在桌面机上复现仅限 Android 的问题。使用时,版本参数要对齐二进制的 Chromium 主版本,取值要描述同一种真实设备。每次更改配置文件或 Chromium 版本后重做页面、Worker 和请求的核对;通过只说明内部一致,不说明站点会如何响应。

#User-Agent#Client Hints#Identity#Ua-Ch#隐私

本文属于指南: 浏览器身份表面:一致性实用总览

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。