前言

大家好,我是 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],转载请注明出处。*