编程 浏览器机器指纹生成原理详解

2026-09-02 11:47:31

浏览器机器指纹生成原理详解

从授权码绑定的实际问题说起

授权码绑定的核心前提是:能找到一台设备上稳定且唯一的“机器指纹”。稳定指同一台机器每次生成结果一致,唯一指不同机器结果不同。否则就会出现授权码绑错了、换浏览器就失效、或者被复制到别的机器上照样能用的问题。下面记录当前方案的具体实现和取舍。

一、指纹由哪几类信息构成

整体公式:

指纹 = Hash(Navigator信息 + Screen信息 + Canvas指纹 + 时区)

这四类信息共同决定一个设备的外观。

1. Navigator 属性

采集以下值:

navigator.userAgent           // "Mozilla/5.0 (Windows NT 10.0; Win64; x64)..."
navigator.language            // "zh-CN"
navigator.languages           // ["zh-CN","zh","en"]
navigator.platform            // "Win32"
navigator.hardwareConcurrency // 8
navigator.deviceMemory        // 8(部分浏览器不支持)
navigator.cookieEnabled       // true
navigator.doNotTrack          // "1" / "0" / "unspecified"

稳定性依据:同一台机器、同一个浏览器,这些值基本不变。UA 会包含操作系统和浏览器版本,只有换浏览器或大版本升级才变化。

2. Screen 属性

screen.width + 'x' + screen.height  // "1920x1080"
screen.colorDepth                    // 24
screen.pixelDepth                    // 24

稳定性依据:用户很少修改屏幕分辨率;色深和像素深度更是极少变动。

3. Canvas 指纹(最关键的一环)

核心代码:

var canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 50;
var ctx = canvas.getContext('2d');

ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 100, 20);

ctx.fillStyle = '#069';
ctx.fillText('auth_fp_' + navigator.userAgent.slice(-10), 2, 15);

ctx.fillStyle = 'rgba(102,204,0,0.7)';
ctx.fillText('auth_fp', 4, 17);

// 关键:将 canvas 内容转为 base64 编码的 PNG
components.push(canvas.toDataURL());

为什么它是最可靠的区分手段:同样的绘图指令,在不同机器上渲染出的像素结果并不完全一样。差异来源包括:

差异来源说明
GPU不同显卡的渲染管线、抗锯齿算法不同
字体渲染引擎Windows 用 ClearType,Mac 用 Quartz,Linux 用 FreeType
字体可用性同样是 "14px Arial",不同系统可能匹配到不同物理字体文件
驱动版本显卡驱动更新可能改变渲染结果

因此,Windows + Chrome 输出的 PNG 和 Mac + Safari 输出的 PNG 在像素级别不同,toDataURL() 得到的 base64 字符串也不同。

稳定性依据:同一台机器、同一个浏览器、同一套字体和驱动,渲染结果完全一致。除非更换显卡、重装系统、或浏览器大版本升级改变渲染引擎。

4. 时区

Intl.DateTimeFormat().resolvedOptions().timeZone  // "Asia/Shanghai"

稳定性依据:用户一般不改时区。

二、如何转成一个固定长度的短字符串

1. 拼接

把所有信息用 | 分隔:

Mozilla/5.0...|zh-CN|zh-CN,zh,en|Win32|8|8|true|0|1920x1080|24|24|data:image/png;base64,iVBORw0KG...|Asia/Shanghai

2. 选用 DJB2 变体 hash 的原因

当前代码用的是 DJB2 变体,而不是 Web Crypto 的 SHA256。原因很实际:Web Crypto API 的 SHA256 是异步的,而指纹生成流程需要同步返回结果,引入一个异步函数会打乱整个授权码校验链路。DJB2 实现简单,同步执行,满足当前场景的抗碰撞需求。

var hash = 0;
for (var i = 0; i < str.length; i++) {
    var char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;  // 等价于 hash * 31 + char
    hash = hash & hash;                   // 截断为 32 位整数
}

逐行说明:

  1. hash << 5 是左移 5 位,即乘以 32;减去 hash 相当于乘以 31;再加上当前字符的 charCode。这样每个字符都参与散列。
  2. hash & hash 把可能溢出的大数截断到 32 位有符号整数范围(等价于 hash | 0hash >>> 0)。
  3. (hash >>> 0).toString(16) 把 32 位有符号整数转为无符号,再转十六进制。
  4. 补长到 32 位:
while (hexHash.length < 32) {
    hexHash += (hash >>> 0).toString(16);
    hexHash = hexHash.substring(0, 32);
}

因为 32 位 hash 的十六进制最多 8 位,重复拼接后截断到 32 位,让指纹看起来更长更像 UUID。

3. 最终结果

e09b0599e09b0599e09b0599e09b0599

这就是最终看到的 32 位机器指纹。

三、完整流程

浏览器加载脚本
  ├─ 收集 Navigator 属性
  ├─ 收集 Screen 属性
  ├─ 创建 Canvas 画固定图形,toDataURL() 得到 base64
  ├─ 收集时区
  ▼
用 "|" 拼接所有信息
  ▼
DJB2 Hash → 32位整数 → 十六进制 → 补长到32位
  ▼
最终指纹(如 e09b0599e09b0599e09b0599e09b0599)
  ▼
随授权请求发送到服务端
  ▼
服务端:首次激活存库,后续比对;不匹配则返回“设备不匹配”

四、刷新不变、换机器变的验证逻辑

刷新后,UA、语言、分辨率、Canvas 渲染结果、时区均不变,所有输入都不变,hash 自然不变。

换机器后,至少以下之一不同:

  • UA(操作系统/浏览器版本不同)
  • 屏幕分辨率
  • Canvas 渲染(不同显卡/字体渲染引擎,这是最可靠的区分手段)
  • CPU 核心数

任一输入变化,hash 结果就完全不同。

五、方案的优缺点与边界

优点

  • 纯前端实现,不需要后端采集。
  • 无需用户操作,自动采集。
  • Canvas 指纹难以手动伪造。
  • 实现简单,不到 60 行。

缺点与已知边界

缺点说明改进方向
浏览器升级后可能变化UA 变了,或者 Canvas 渲染引擎变了只取 UA 的操作系统部分,忽略浏览器版本
隐私模式可能不同部分浏览器隐私模式下 Canvas 渲染会加噪声提示用户不要用隐私模式
hash 碰撞理论存在DJB2 不是加密级 hash改用同步的 SHA256 库(如 jsSHA)
字体影响 Canvas安装/卸载字体可能改变渲染结果用更多维度降低单一维度影响权重

可选的进一步提升方向

  1. 加入 WebRTC 获取内网 IP。
  2. 加入 AudioContext 指纹,不同声卡处理同一音频的输出有微小差异。
  3. 加入 WebGL 指纹,通过渲染器信息获取显卡型号。
  4. 用同步 SHA256 库替代 DJB2。

当前方案用于“授权码绑定设备”已经够用,不需要追求银行级唯一性。

复制全文 生成海报 浏览器 指纹 前端 安全

推荐文章

程序员茄子在线接单