如何用 JavaScript 获取浏览器指纹:代码示例、局限与隐私规则
用 JavaScript 获取浏览器指纹,做法是读取浏览器公开的一组属性(User-Agent、语言、时区、屏幕尺寸、硬件信息),再加上 Canvas 绘图这类渲染测试,把这些值拼成一个字符串,用 Web Crypto API 算出哈希。得到的是一个概率性的标识,并不是稳定的 ID;在欧盟,采集它通常需要征得用户同意。
本文用原生 JavaScript 把这个过程走一遍,讲清为什么结果变化得比想象中频繁,并说明相关的隐私规则。适合想了解自己浏览器会暴露什么信息的开发者,也适合做风控和账号安全功能的团队。各类指纹技术的完整介绍,见我们的浏览器指纹技术详解。
什么是浏览器指纹
W3C 关于减少浏览器指纹的指导文件,把浏览器指纹定义为:网站通过配置设置或其他可观察特征,识别或再次识别访问用户、用户代理或设备的能力。文件把它分为两类:
- 被动指纹:只用每次请求自带的信息,例如 HTTP 请求头和 IP 地址,不需要在设备上运行代码。
- 主动指纹:在设备上运行 JavaScript 等代码,观察更多特征,例如字体、窗口大小或设备传感器。
本文讲的都是主动指纹:页面向浏览器提问,并记录回答。MDN 术语表列出的典型组成包括:浏览器版本、时区和首选语言、可用的音视频编解码器、已安装字体、浏览器设置状态,以及屏幕尺寸和分辨率。
第一步:采集基础属性
大部分属性来自标准对象,不会弹出任何权限提示:
function collectAttributes() {
return {
userAgent: navigator.userAgent,
languages: (navigator.languages || []).join(','),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: [screen.width, screen.height, screen.colorDepth].join('x'),
pixelRatio: window.devicePixelRatio,
cores: navigator.hardwareConcurrency || null,
memory: navigator.deviceMemory || null,
touchPoints: navigator.maxTouchPoints || 0
};
}有两点要注意。navigator.deviceMemory 只在基于 Chromium 的浏览器里提供,其他浏览器返回 undefined,所以代码里存成 null。另外,Chromium 系浏览器已经精简了默认的 User-Agent 字符串,把细节移到了 User-Agent Client Hints 里,单看 User-Agent 能拿到的信息比以前少。
第二步:加入 Canvas 渲染测试
区分度主要来自渲染测试。页面在一块不显示的画布上绘制文字和图形,再把结果读回来。不同机器在字体渲染、抗锯齿和图形硬件上有细微差别,输出也就不同:
function canvasSignal() {
const canvas = document.createElement('canvas');
canvas.width = 240;
canvas.height = 60;
const ctx = canvas.getContext('2d');
if (!ctx) return 'no-canvas';
ctx.textBaseline = 'top';
ctx.font = '16px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(100, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('fingerprint test 123', 2, 15);
return canvas.toDataURL();
}这块画布不会加到页面上,访客看不到任何东西。WebGL 和音频处理也有类似的测试,套路相同:渲染、读回、算哈希。
第三步:合并并计算哈希
Canvas 输出的 data URL 很长,所以最后一步通常是把所有值哈希成一个短值。Web Crypto API 的 crypto.subtle.digest 支持 SHA-256。据 MDN 说明,它只能在安全上下文中使用,也就是通过 HTTPS 或 localhost 访问的页面:
async function sha256Hex(text) {
const bytes = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest('SHA-256', bytes);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
async function getFingerprint() {
const data = collectAttributes();
data.canvas = canvasSignal();
const keys = Object.keys(data).sort();
const text = keys.map(k => k + '=' + data[k]).join('|');
return sha256Hex(text);
}
getFingerprint().then(id => console.log('fingerprint', id));对字段名排序,是为了让拼接顺序每次一致,相同的输入总能得到相同的哈希。
为什么指纹总在变
实际跑一下就会发现,这个哈希没有想象中稳定。常见原因有:
- 浏览器更新会改变 User-Agent 字符串,有时也会改变渲染结果,一次更新就可能产生新的哈希。
- 屏幕和窗口变化,例如接了外接显示器或调整了缩放比例,屏幕和像素比的值随之改变。
- 浏览器的主动防护。 各家浏览器都在限制指纹采集。Brave 会给 Canvas、WebGL 和音频结果加入细微的随机化,让同一个浏览器在不同网站、不同会话之间看起来都不一样;Firefox 和 Safari 会限制或模糊化脚本能读到的信息;Tor 浏览器则尽量让所有用户看起来一样。
- 隐私窗口或嵌入式环境中,部分 API 受限或返回默认值。
所以单独一个完整哈希并不适合当标识。生产环境里的系统一般会逐项比对信号、接受部分匹配,再结合其他证据综合判断。为什么各项数值之间的一致性比「独一无二」更重要,可以参考我们的浏览器指纹检测结果怎么看。
隐私与法律规则
W3C 的指导文件指出了指纹敏感的原因:和 Cookie 不同,浏览器指纹通常无法被用户清除或重置,即使用户主动删除了 Cookie,也可能被再次识别。
在欧盟,欧洲数据保护委员会(EDPB)发布的《关于电子隐私指令第 5(3) 条技术适用范围的第 2/2023 号指南》(最终版于 2024 年 10 月通过)明确,关于访问用户设备中信息的规定不只适用于 Cookie,也可以适用于设备指纹技术。实际做法是:除非处理对用户主动请求的服务确属必要,否则要先征得同意。涉及个人数据时,还要同时遵守 GDPR。在生产环境采集指纹之前,请先和法务确认适用规则。
在风控、账号安全这类正当用途中,建议做到:
- 只采集必要的信号,并在隐私政策中写明。
- 保存哈希而不是原始值,并设定保存期限。
- 不用指纹在互不相关的网站之间追踪用户。
对广告运营意味着什么
广告平台在自己的页面上也会读取很多同样的信号,这是投放团队关心指纹的原因。理解这些信号,有助于把本来就应当分开的环境(例如不同客户、不同团队成员)配置得一致、规范。但它不是绕开平台规则的手段:Meta、Google、TikTok 都禁止任何规避审核或执法的做法,任何指纹配置都改变不了这一点。
常见问题
JavaScript 能拿到唯一的浏览器 ID 吗?
不能可靠地拿到。JavaScript 可以把属性和渲染测试合成一个往往很有区分度的哈希,但浏览器更新、显示设置变化和内置防护都会让它随时间改变,只能当作概率性信号。
采集浏览器指纹需要用户同意吗?
在欧盟通常需要。EDPB 第 2/2023 号指南说明,电子隐私指令第 5(3) 条可以适用于设备指纹,除非处理对用户请求的服务确属必要,否则需要征得同意。
为什么页面上 crypto.subtle 用不了?
crypto.subtle 只能在安全上下文中使用。请通过 HTTPS 提供页面,或在 localhost 上测试。
浏览器能拦截指纹脚本吗?
浏览器会减少脚本能获取的信息。Brave 会随机化 Canvas、WebGL 和音频输出,Firefox 和 Safari 会限制部分 API,Tor 浏览器则把大量数值统一,让用户看起来彼此相同。