前言
大家好,我是 Lanmei。今天想和大家聊一个在 Web 安全与反爬虫领域非常基础但又极其重要的概念——浏览器指纹。
如果你做过爬虫开发、网站数据分析,或者单纯对"网站怎么知道是我"这件事感兴趣,那这篇文章应该会对你有所帮助。本文面向入门读者,力求把"浏览器指纹是什么、怎么采、有什么用"讲清楚。
一、什么是浏览器指纹?
在现实生活中,每个人的指纹都是独一无二的——哪怕同卵双胞胎,指纹纹路也完全不同。浏览器指纹(Browser Fingerprinting)借用了这个类比,指的是一组由浏览器和设备环境信息构成的特征组合,能够在一定概率下区分不同的用户。
换句话说:你的浏览器暴露给网站的各种属性(屏幕大小、操作系统、显卡型号、字体列表等)拼接在一起,形成了一个很像"指纹"的标识。
为什么网站要采集指纹?
这个问题可以反过来想:网站有什么动机要识别你?
| 场景 | 需求 |
|------|------|
| 反爬虫 | 识别自动化工具,拦截恶意流量 |
| 反欺诈 | 检测账号盗用、刷单、垃圾注册 |
| 用户追踪 | 跨站点追踪,尤其是在 Cookie 被限制后 |
| 个性化 | 记住你的偏好,提供一致体验 |
| 安全风控 | 登录异常检测,二次验证触发 |
简单概括:网站希望知道屏幕对面是不是同一个人,而浏览器指纹是实现这一目标的重要技术手段。
二、发展历程:从 Cookie 到 Fingerprint
2.1 Cookie 时代
早期网站靠 HTTP Cookie 来标识用户——服务端下发一个随机字符串,浏览器每次请求都带上它。这种方式简单可靠,但弊端也很明显:
- 用户可以主动清除 Cookie
- 浏览器可以禁用 Cookie
- 隐私法规(如 GDPR)要求 Cookie 需经用户同意
2.2 超级 Cookie / Evercookie
有人想绕过 Cookie 限制,于是出现了"超级 Cookie"——利用 Flash SharedObject、Silverlight IsolatedStorage、HTTP ETag、IndexedDB 等多种存储机制互相备份,清除一个还能从另一个恢复。这种方式攻击性太强,随着 Flash 退役和浏览器安全策略收紧,已经基本消亡。
2.3 浏览器指纹时代
真正让指纹技术进入主流的是 2010 年 EFF(电子前哨基金会)的 Panopticlick 项目。它首次公开验证了:仅凭浏览器发送给网站的 HTTP 头信息和 JavaScript 可访问的属性,就能以极高的熵值唯一标识一台设备。
里程碑事件:
| 时间 | 事件 |
|------|------|
| 2010 | EFF 发布 Panopticlick,首次大规模测试浏览器指纹 |
| 2012 | Canvas 指纹技术被公开提出 |
| 2014 | AudioContext 指纹出现 |
| 2017 | FingerprintJS 开源项目发布 |
| 2020+ | Apple/Safari、Firefox 开始主动屏蔽指纹采集 API |
从"存标识"到"算标识",这是根本性的范式转变。
三、基础指纹信息
我们先从最容易获取的基础信息开始。这些都是 JavaScript 可以直接读取或推断的浏览器属性。
3.1 User-Agent(用户代理)
User-Agent 字符串包含操作系统、浏览器名称和版本、渲染引擎等信息。
function getUA() {
return navigator.userAgent;
}
// 示例输出:
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...
3.2 屏幕分辨率与色深
function getScreenInfo() {
return {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelRatio: window.devicePixelRatio
};
}
注意:`screen.width` 是物理分辨率,`window.innerWidth` 是视口大小,两者不同。
3.3 时区
function getTimezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
// "Asia/Shanghai"
}
3.4 语言偏好
function getLanguages() {
return {
language: navigator.language, // "zh-CN"
languages: navigator.languages, // ["zh-CN", "en-US"]
};
}
3.5 字体列表
字体检测的方式比较巧妙:利用 document.fonts API 加上 Canvas 测量文本宽度来推断字体是否存在。
async function getFontList() {
await document.fonts.ready;
const fontSet = document.fonts;
const fonts = [];
fontSet.forEach(f => fonts.push(f.family));
return fonts;
}
但更常用的方式是用一个预定义字体列表,逐个在 Canvas 上渲染并检测宽度变化,以此判断字体是否可用。
基础信息虽然单个来看区分度不高,但组合起来已经能提供可观的熵值。
四、高级指纹技术
基础信息可以通过修改浏览器配置来改变,但高级指纹利用了更底层的硬件和渲染差异,同一台设备在不同浏览器上呈现出相似特征,更难伪造。
4.1 Canvas 指纹
这是最经典的浏览器指纹技术,由 Miele 等人在 2012 年提出。
原理: 不同操作系统、显卡、浏览器在处理 Canvas 2D 图形的亚像素渲染时存在微小差异,这些差异体现在最终像素上。通过绘制相同的内容(通常是文本 + 几何图形),然后提取像素数据,就能得到设备相关的特征值。
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
const ctx = canvas.getContext('2d');
// 绘制背景
ctx.fillStyle = '#f8f8f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制文本(字体渲染差异是关键)
ctx.fillStyle = '#1a1a1a';
ctx.font = '18px Arial';
ctx.fillText('BrowserFingerprint', 10, 40);
ctx.font = '14px sans-serif';
ctx.fillText('Hello, 你好, こんにちは', 10, 70);
// 绘制图形
ctx.strokeStyle = '#4a90d9';
ctx.beginPath();
ctx.arc(200, 60, 30, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.arc(200, 60, 15, 0, Math.PI * 2);
ctx.fill();
// 获取像素数据
const dataUrl = canvas.toDataURL();
// 计算哈希
return hashString(dataUrl);
}
// 简单的哈希函数
function hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return hash >>> 0; // 转为无符号整数
}
关键点: 即使相同的绘制代码,在不同设备上 toDataURL() 返回的 base64 字符串也不同。
4.2 WebGL 指纹
WebGL 让我们可以访问 GPU 的信息,包括显卡型号、驱动程序版本、渲染能力等。这些信息对设备极为敏感。
function getWebGLFingerprint() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return null;
return {
vendor: gl.getParameter(gl.VENDOR), // "Google Inc."
renderer: gl.getParameter(gl.RENDERER), // "ANGLE (AMD, ...)"
version: gl.getParameter(gl.VERSION), // "WebGL 2.0"
shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
extensions: gl.getSupportedExtensions(),
// 还可以渲染 3D 场景提取像素数据
};
}
WebGL 的 RENDERER 字段经常暴露具体显卡型号,比如 `ANGLE (AMD Radeon Pro 5500M)`。这也是为什么不少网站在风控时会选择直接采集 WebGL 信息。
4.3 AudioContext 指纹
这是利用浏览器处理音频信号时的底层差异——不同设备对音频数据的处理(尤其是波形生成、PCM 编码过程中的浮点运算精度)存在微小差异。
function getAudioFingerprint() {
return new Promise(resolve => {
const audioCtx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 44100, 44100);
const oscillator = audioCtx.createOscillator();
const compressor = audioCtx.createDynamicsCompressor();
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(1000, 0);
// 配置压缩器参数
compressor.threshold.setValueAtTime(-50, 0);
compressor.knee.setValueAtTime(40, 0);
compressor.ratio.setValueAtTime(12, 0);
compressor.attack.setValueAtTime(0, 0);
compressor.release.setValueAtTime(0.25, 0);
oscillator.connect(compressor);
compressor.connect(audioCtx.destination);
oscillator.start(0);
oscillator.stop(1);
audioCtx.oncomplete = function(event) {
const buffer = event.renderedBuffer;
const data = buffer.getChannelData(0);
let hash = 0;
// 对前 1000 个采样点取哈希
for (let i = 0; i < 1000; i++) {
hash = ((hash << 5) - hash) + (data[i] * 10000 | 0);
hash |= 0;
}
resolve(hash >>> 0);
};
audioCtx.startRendering();
});
}
AudioContext 指纹是目前公认稳定性高于 Canvas 指纹的技术之一——它在同一设备上产生的结果高度一致,跨浏览器差异也较为明显。
五、FingerprintJS 原理分析
[FingerprintJS](https://github.com/fingerprintjs/fingerprintjs) 是目前最流行的开源浏览器指纹库,目前在 GitHub 上有 22k+ stars。我们来拆解它的核心原理。
5.1 核心流程
用户访问页面
│
▼
采集 40+ 个特征信号
│
├── 浏览器层:UA、语言、平台、插件列表、DoNotTrack ...
├── 渲染层:Canvas、WebGL、AudioContext
├── 设备层:屏幕、CPU核心数、内存、电池状态
├── 存储层:Cookie 可用性、IndexedDB、LocalStorage
└── 网络层:IP、时间偏移、地理位置(推算)
│
▼
信号标准化(不同浏览器返回格式不同,需归一化)
│
▼
信号加权 → 熵值计算 → 哈希 → 生成 visitorId
5.2 核心特点
1. 离线计算:所有指纹计算在浏览器端完成,不依赖后端服务
2. 概率唯一性:不保证 100% 唯一,但对绝大多数场景足够
3. 稳定性优先:不采集容易变化的信号(如 IP 地址),优先采设备固有属性
4. 增量更新:新版本会添加新的特征信号来提升区分度
5.3 简单使用
// 通过 CDN 引入
//
async function getFingerprint() {
const fp = await FingerprintJS.load();
const result = await fp.get();
console.log('Visitor ID:', result.visitorId);
console.log('Confidence:', result.confidence); // 置信度 (0~1)
console.log('Components:', result.components); // 所有特征信号
}
getFingerprint();
FingerprintJS v3+ 使用了超过 40 个特征维度,据官方数据,不同设备的碰撞概率低于万亿分之一。
六、完整采集示例
下面是一个可以直接运行的浏览器指纹采集页面,整合了上述所有技术点:
浏览器指纹采集 Demo
🔍 浏览器指纹采集
点击按钮查看结果...
你可以直接复制这段代码保存为 HTML 文件,在浏览器中打开运行,看看自己的指纹是多少。
七、浏览器指纹在反爬虫中的应用
指纹采集本身是中性的技术,但在反爬虫场景中尤为常见。下面梳理几个典型的应用方式。
7.1 爬虫检测链路
用户请求 → 采集指纹 → 与已知指纹库对比
│
┌─────────┴──────────┐
▼ ▼
命中已知爬虫指纹 未命中 → 进入风控规则引擎
│
┌─────┴─────┐
▼ ▼
正常访问 触发验证码或拦截
7.2 常见判定依据
| 指纹特征 | 爬虫/自动化工具的典型表现 |
|----------|------------------------|
| WebGL Renderer | 缺少真实 GPU,通常为 "SwiftShader" 或 "Mesa" |
| 屏幕分辨率 | 固定值(如 1920×1080),缺少正常人类应有的多样性 |
| navigator.webdriver | Puppeteer/Playwright 下为 true |
| Canvas 指纹 | 渲染结果与真实浏览器不符(字体缺失、抗锯齿不同) |
| Chrome 插件列表 | 空列表(真实用户通常会装插件) |
| 时间行为 | 访问间隔过于均匀,不符合人类阅读节奏 |
7.3 局限性
浏览器指纹在反爬虫中不是银弹。以下场景指纹效果会大打折扣:
- **指纹已泄露**:指纹值被提取后,爬虫可以伪造特征
- **对抗升级**:Puppeteer Stealth、Playwright 等工具已经能模拟大部分指纹
- **共享设备/机房 IP**:同一个出口 IP + 统一的 Docker 环境,大量设备指纹趋同
- **隐私保护浏览器**:Tor Browser 默认禁用 Canvas 和 WebGL
因此,工业级反爬虫方案从来都是多重维度防守——指纹 + IP 信誉 + 行为分析 + 设备环境 + 业务特征,缺一不可。
八、指纹唯一性评估:熵值与碰撞
8.1 什么是熵(Entropy)?
借用信息论的概念,熵衡量的是信息的"不确定性"或"信息量"。在指纹场景中,熵值越大,这个指纹能区分的设备就越多。
公式很简单(香农熵):
H(X) = -Σ p(x) · log₂ p(x)
单位是比特(bit)。一个特征的熵值是 10 bit,意味着它可以区分 2¹⁰ ≈ 1024 个不同的值。
8.2 各特征的典型熵值估算
参考 EFF 在 2020 年更新的数据(误差范围 ±20%):
| 特征 | 估算熵值 | 可区分设备数 |
|------|---------|-------------|
| User-Agent | ~10 bit | ~1024 |
| 屏幕分辨率 + 色深 | ~6 bit | ~64 |
| 时区 | ~5 bit | ~32 |
| 系统字体列表 | ~13 bit | ~8192 |
| Canvas 指纹 | ~8 bit | ~256 |
| WebGL Renderer | ~6 bit | ~64 |
| AudioContext 指纹 | ~7 bit | ~128 |
| 全部组合 | **~33-35 bit** | **约 80亿 - 340亿** |
8.3 碰撞概率
我们可以用生日悖论来计算碰撞概率:在 N 个可能的指纹值中,随机抽取 k 个用户,至少出现一次碰撞的概率约为:
P(k, N) ≈ 1 - exp(-k² / 2N)
假设指纹空间为 2³³ ≈ 85 亿:
| 用户数量 | 至少一次碰撞的概率 |
|----------|-------------------|
| 10,000 | ≈ 0.00006% |
| 100,000 | ≈ 0.006% |
| 1,000,000| ≈ 0.58% |
| 10,000,000| ≈ 44% |
也就是说,如果你有 1000 万用户,大约有 44% 的概率至少有两个用户的指纹发生碰撞。这就是为什么工业级实现从来不依赖单一指纹值,而是结合多个信号做综合判断。
8.4 为什么要关注熵
熵的概念对你做工程决策有直接帮助:
- **新增一个特征前**:先算它贡献了多少熵值,低于 1 bit 的可以不采
- **特征权重调整**:熵值高的特征(如字体列表)应给予更高权重
- **稳定性与熵值的取舍**:有些特征熵值高但变化快(如 IP),需要用稳定性系数来加权
九、写在最后
浏览器指纹是一项引人入胜的技术——它既在反欺诈、反爬虫中扮演着关键角色,又时刻触及**用户隐私**的敏感神经。
我个人的看法是:
- **作为开发者**,理解指纹技术能帮你写出更好的安全方案
- **作为使用者**,了解指纹采集方式能让你更有意识地保护自己的隐私
- **作为行业参与者**,需要在业务需求和用户隐私之间找到合理的平衡点
这篇文章只是入门。如果你对更深入的话题感兴趣,后续我会接着写:
- 浏览器指纹的稳定性测试与长期追踪
- 指纹伪造与反伪造的攻防对抗
- 如何在 Node.js 服务端验证和存储指纹
- 使用 WebAssembly 加速指纹计算
欢迎在评论区留言交流,也欢迎大家关注后续文章。
Happy Hacking 🚀
*本文首发于 [Blog],转载请注明出处。*
评论