浏览器指纹如何计算:从采集信号到验证隐私设置
清掉 Cookie,网站就认不出这个浏览器了吗?
Cookie 是网站存进浏览器的一份数据,删除后,这份标记就没了。但浏览器的语言、时区、字体和图形渲染方式通常还在。网站可以重新读取这些特征,算出一个标记,再与之前的访问记录比较。这就是浏览器指纹的基本思路。
Fingerprints are the new cookies 把它做成了一个小实验:页面根据浏览器特征生成昵称,请读者刷新、清 Cookie、换网络,观察昵称是否变化。顺着这个演示和它的源码,可以看清两件事:网站用什么计算指纹,以及隐私设置究竟改变了其中哪一步。
1. 网站为什么要用浏览器指纹
账号安全和反欺诈是一类用途。比如,一个账号突然从陌生的浏览器环境登录,或者多个新账号看起来来自同一个环境,网站可以把这些线索和登录记录、操作频率一起用于风险判断,再决定是否要求额外验证。Fingerprint 的产品文档就列出了账号盗用防护、重复注册和优惠券滥用等应用。Fingerprint 应用场景
网站也可能用它辅助识别回访,把多次访问关联起来,分析访问行为或提供个性化内容。这里关心的是“这次访问是否可能来自之前那个浏览器”,即使用户没有登录,也可能进行这种判断。W3C 对访问关联的说明、Fingerprint 应用场景
广告追踪会进一步利用这种关联能力。如果不同网站上的追踪服务取得了可比较的特征,就可能把跨站浏览记录拼接起来,用于建立兴趣画像。用户即使清除了 Cookie,也未必能切断这种关联,因为浏览器仍可能暴露相同的特征。这正是指纹带来的隐私问题之一。W3C 指纹隐私说明
这些判断都有边界。不同浏览器可能提供相同或相近的特征,同一浏览器的特征也可能随更新、设置和硬件变化。指纹只能提供关联线索,不能单独证明两次访问来自同一个人。接下来先看网页究竟能读到什么,再亲手检查这些读数是否稳定、是否会随隐私设置改变。
2. 先看看网页读到了什么
那串字符是哈希,可以先把它理解成这组测量结果的摘要。下面的名称则对应参与测量的项目。后面的实验需要比较这些内容,所以先记下浏览器版本、已启用的扩展和隐私设置,再保存一份结果。
打开开发者工具的 Console,在原文页面运行下面这段代码。它调用网页已经使用的模块,把结果打印成 JSON,复制保存即可。
浏览器指纹如何计算:从采集信号到验证隐私设置 (javascript)
await (async () => {
const module = await import('/public/nickname.js');
const measurement = await module.fingerprint();
const record = {
capturedAt: new Date().toISOString(),
page: location.href,
userAgent: navigator.userAgent,
measurement
};
console.log(JSON.stringify(record, null, 2));
})();先看
measurement 里的三个地方就够了:sources 是各项测量结果,clean 列出脚本最终采用的项目,hex 是算出的完整哈希。外面的时间、网址和 userAgent 是为实验补充的记录,不参与原演示的计算。每次想重新测量,都先刷新页面。原模块会记住本次页面加载的结果,连续执行代码只是重复取出这份结果。如果模块被浏览器拦住了,就记下这个现象,不必为了跑通代码关闭保护。模块返回值与缓存逻辑
3. 这些特征是怎么测出来的
普通网页不用申请麦克风权限,也能得到音频指纹;不用截取桌面,也能得到图形指纹。原因在于,网页可以让浏览器完成一些计算,再读取计算结果。
原演示一共用了七项测量。按它们的工作方式,可以分成下面几组。
读取浏览器直接提供的信息
最容易理解的是语言、时区和硬件相关属性。网站本来就需要其中一些信息来选择语言、显示当地时间或安排计算任务,这些信息组合起来,也能用于区分浏览器。
演示的
hardware 包含浏览器报告的逻辑处理器数量、近似内存容量和语言。这里的数值不一定等于真实硬件规格:浏览器可以少报可用处理器数量,内存也只报告近似值,有些浏览器甚至不提供这一项。处理器数量文档、内存属性文档另一项叫
time zone,除了时区,还包含区域、日历、数字系统、小时制和数字格式。这些设置主要通过 Intl API 读取。换一个网络出口,通常不会顺带改变电脑的时区和语言,所以这一项可能保持不变。Intl 文档画同一幅图,再比较像素
Canvas 是网页上的画布。脚本可以在上面画文字、emoji 和渐变,再用
getImageData() 读取每个像素。即使绘制指令相同,字体、操作系统和渲染实现不同,最后的像素也可能有细微差异。原演示把这些像素算成摘要,记为 canvas。这里读的是脚本自己画出的内容。它没有截取桌面,也没有列出电脑上的全部字体;但某个指定字体是否存在、缺失时用什么字体替代,都可能影响结果。Canvas 像素读取文档
WebGL 则让网页调用图形渲染能力。原演示从中取得三项不同的数据:
名称 | 实际测量的内容 |
|---|---|
GPU | 浏览器报告的图形渲染器信息,清理后留下部分厂商、型号信息。 |
WebGL | 支持哪些扩展、纹理等资源的上限,以及着色器计算精度。 |
renderer | 画一个三角形,再用 readPixels() 读取像素,计算摘要。 |
这三项分别观察报告的信息、支持的能力和实际画出的结果。因此,GPU 名称相同,不代表后两项一定相同。隐私设置也可能限制型号信息的读取,不能把网页拿到的字符串当成完整的硬件清单。WebGL 调试扩展、像素读取方法
生成一段声音,只读取计算结果
音频指纹采用类似的方法。脚本通过
OfflineAudioContext 生成一段三角波,经过动态压缩器处理,最后从缓冲区取出采样值。整个过程都可以在内存里完成,既不录音,也不播放声音。演示取其中一段采样,对绝对值求和,保留三位小数,得到
audio。音频处理实现及运行环境可能影响结果,舍入则会抹掉一部分微小差异。原演示的音频实现、OfflineAudioContext 文档到这里,七项就齐了:
hardware、time zone、canvas、GPU、WebGL、renderer 和 audio。它们大多看起来很普通,辨识能力来自组合。同一种语言的人很多,同一款显卡的人也很多,两者再加上渲染和音频结果,符合条件的浏览器就可能少得多。原文还谈到了 IP、请求头、屏幕尺寸等信息。当前演示没有把它们作为独立项目直接加入哈希,不过会向服务器询问访问是否来自 Tor 出口,用这个判断选择部分规则和昵称。后面换网络时,需要记住这点。演示实际采用的输入
4. 把测量结果算成一个指纹
拿到数据之后,脚本先检查读数有没有异常。例如,它会画一块已知颜色的色块,再读回像素。如果读到的颜色和预期不一致,就认为这项结果受到了干预。
最终每项会落入三个状态之一:
clean 表示成功读取、没有触发脚本的检查;tampered 表示读数不一致、已知答案检查失败,或者命中了预设的浏览器规则;unavailable 表示没有拿到数据,例如 API 缺失或读取超时。clean 只代表通过了这套检查,不保证是真实硬件值。这份代码把这些项目的名称和值按固定顺序拼接,编码成字节,再用 SHA-256 算出完整哈希。如果 crypto.subtle 不可用,就改用自己的 FNV-1a 变体;没有任何 clean 项时,则不计算最终哈希。筛选与计算源码哈希方便比较,却不会创造新的辨识信息。两台电脑如果提供了相同输入,就会得到相同摘要;输出有 256 位,也不代表收集到了 256 位独立的身份信息。原文讨论信息熵时还提醒,彼此相关的特征,信息量不能直接相加。哈希 API 文档、原文的信息熵讨论
想亲手核对这一步,可以继续在原页面运行下面的代码。它取出刚才被采用的项目,重新拼接并计算 SHA-256。正常匹配时,Console 会显示
SHA-256 matches;没有 SHA-256 结果可比较时,会给出提示。浏览器指纹如何计算:从采集信号到验证隐私设置 (javascript)
await (async () => {
const { fingerprint } = await import('/public/nickname.js');
const result = await fingerprint();
if (!result.clean.length || result.algo !== 'SHA-256') {
console.log('No SHA-256 result to compare');
return;
}
const lines = [];
for (const name of result.clean) {
lines.push(`${name}:${result.sources[name].value}`);
}
const bytes = new TextEncoder().encode(lines.join('\n'));
const digest = new Uint8Array(
await crypto.subtle.digest('SHA-256', bytes)
);
const calculated = Array.from(digest, byte =>
byte.toString(16).padStart(2, '0')
).join('');
if (calculated !== result.hex) throw new Error('Digest mismatch');
console.log('SHA-256 matches');
})();最后才轮到昵称。通常,脚本根据哈希从两张词表中各选一个词,所以不同哈希也可能得到相同昵称。遇到 Brave 或被判定为 Tor 出口的访问,还会直接显示
Brave Stranger 或 Onion Stranger。这就是为什么实验要保存完整哈希,不能只盯着昵称。Tor 出口判断也不等于确认使用了 Tor Browser。昵称生成代码5. 清 Cookie、开隐私窗口,再看看什么变了
先不要改设置,刷新同一页面三次,每次都保存结果。这样可以知道,当前环境下的读数本身是否稳定。然后分别做下面三组实验。每组都先存一份操作前的结果,做完再测,不要把几种改动叠在一起。
清除本站数据。 在 Firefox 中,可以从地址栏左侧的站点信息入口清除当前站点的 Cookie 与站点数据,再打开原文测试。确认只清理测试站点即可。这项操作会连同该站点的其他存储一起清理,也可能退出本站登录,所以记录时写清“清除了站点数据”,不要把全部变化都归因于 Cookie。Firefox 清理说明
换到隐私窗口。 在隐私窗口测一次,刷新后再测一次,然后关闭全部隐私窗口,重新打开并测试。这里比较的是三种情况:普通与隐私模式、同一次隐私会话内、两次隐私会话之间。在 Firefox 中,关闭全部隐私窗口会结束这次隐私会话并丢弃 Cookie,所以实验时不要留下其他隐私窗口。顺手记下隐私模式里哪些扩展仍然启用。Firefox 隐私浏览说明
切换网络。 保持浏览器、窗口和设置不变,换一个可用网络,再刷新测试。如果希望连 Tor 出口判断一起重测,在切换前后两次采样之前,都先在原页面执行
sessionStorage.removeItem('tor')。这是因为演示把这个判断暂存在当前标签页中,只刷新可能还会使用旧值。缓存实现比较时先看具体项目,再看总哈希。有一项从
clean 变为 unavailable,可能只是读取超时,导致参与计算的数据少了。展开它的 why 字段,就能看到脚本给出的原因。测量期间保持页面前台,可以减少这类干扰。6. 打开防护,再做一轮对照
浏览器可以在不同环节介入:直接拦住采集脚本,给读数加入扰动,或者让不同用户得到更接近的结果。Brave 的随机化设计会按站点和会话保持一定的一致性,Tor Browser 则尽量减少用户之间的可见差异。因此,有保护不一定意味着每次刷新都换一个指纹。Brave 的设计说明、Tor Browser 指纹保护
用桌面 Firefox 做一组具体对照:
- 在标准模式下保存三次测量结果。
- 打开“设置 → 隐私与安全 → 增强型跟踪保护”,切换到严格模式,按提示重新加载页面。
- 再测三次,比较哪些项目的值变了,哪些项目没有进入最终计算。
- 打开常用网页,检查字体、图像、视频和交互有没有异常。如果某页失效,可以用地址栏的盾牌临时关闭本站保护做对照,检查后恢复。
若平时已经使用严格模式,就保留它,检查当前表现即可,不必为了实验降低日常防护。需要完整的标准与严格对照时,另建测试配置文件;自定义模式也可以这样处理。
严格模式一次改变了多项保护,所以这组实验比较的是整套设置的效果。Firefox 既会拦截已知的指纹脚本,也会限制网页能读到的信息。原文演示还能运行,不足以说明这些措施全部失效。Firefox 设置、指纹保护说明
还可以在设置改变前后,分别运行一次 EFF Cover Your Tracks。点击
Test Your Browser,保持测试选项相同,不勾选 Test with a real tracking company。把跟踪器拦截结果和指纹结果分开记录:脚本有没有被拦住,与读到的指纹是否独特,是两个问题。EFF 会收集测试数据用于研究和比较。它报告的唯一性,是相对于所观察到的访问者样本而言的,不是对全球所有浏览器的普查。EFF 方法说明
7. 怎样读懂实验结果
每轮保存的 JSON 已经包含详细数据,另记一张简表,方便把结果和操作对应起来:
做了什么 | 哪些项目发生变化 | 完整结果文件 | 网页是否正常 |
|---|---|---|---|
普通刷新 | 填实际观察 | 填 JSON 文件名 | 填检查结果 |
清理数据、隐私模式或切换网络 | 每项操作单独一行 | 填 JSON 文件名 | 填检查结果 |
标准模式改为严格模式 | 填实际观察 | 填 JSON 文件名 | 填检查结果 |
如果清理站点数据后,参与计算的项目和哈希都没变,就说明这次演示仍能从当前环境算出同一个结果。如果哈希变了,往前检查是哪项值或状态变化了,而不是立即判断保护成功。
这个区别很重要:演示采用的是整组数据的哈希,输入稍有变化,摘要通常就不同;实际追踪系统还可能比较其他特征,把两次访问关联起来。反过来,几次测量都相同,也只能说明它在这些测量中稳定,不能证明它在人群中唯一。原文对演示局限的说明
最终要留下的,是具体观察:改了哪个设置,哪些数据随之改变,哪些仍然可读,常用网页是否受影响。有了这些记录,再决定要不要保留某项设置,比单看页面上的昵称或一句保护评分有用得多。
本文依据 2026-10-12 读取的原文、演示源码与官方文档整理。两段代码已在独立 Chromium 会话中运行并检查;清理数据、隐私窗口、网络切换及 Firefox 设置的完整对照实验尚未执行。