3. 设备指纹采集原理:JavaScript API调用、硬件信息获取、软件环境探测
设备指纹怎么来的?说白了,就是浏览器主动告诉我们的。你想想看,每次打开一个网页,浏览器都会暴露一大堆信息。我刚开始做前端安全时,也觉得这玩意儿挺神奇的——浏览器居然能吐出这么多数据。
采集原理分三大块:JavaScript API调用、硬件信息获取、软件环境探测。这三块组合起来,就能拼出一张比较完整的设备画像。
3.1 JavaScript API调用:浏览器主动暴露的信息
浏览器为了支持现代Web应用,开放了大量API。这些API本身不是为指纹设计的,但我们可以拿来做指纹采集。嗯,这里要注意:不要调用敏感权限API(比如摄像头、麦克风),否则会触发用户授权弹窗,反而暴露了采集行为。
3.1.1 navigator对象
navigator对象是信息大户。我个人习惯先从这里下手:
// 基础信息采集
const fingerprint = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
languages: navigator.languages,
hardwareConcurrency: navigator.hardwareConcurrency, // CPU逻辑核数
deviceMemory: navigator.deviceMemory, // 设备内存(GB)
maxTouchPoints: navigator.maxTouchPoints,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
webdriver: navigator.webdriver // 是否被自动化工具控制
};
navigator.deviceMemory在某些浏览器返回undefined的情况。这个API目前只有Chromium系支持,Safari和Firefox直接忽略。采集时一定要做fallback处理。
3.1.2 screen对象
屏幕信息也是指纹的重要维度。我记得有一次排查一个指纹冲突问题,最后发现是两个同型号手机在屏幕参数上完全一致——嗯,这说明单靠屏幕信息不够,得组合其他维度。
const screenInfo = {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth,
orientation: screen.orientation ? screen.orientation.type : null
};
3.1.3 时间与时区
时区偏移量是个容易被忽略但区分度很高的特征。不同设备即使设置相同时间,时区偏移也可能不同:
const timezoneOffset = new Date().getTimezoneOffset();
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
3.2 硬件信息获取:从浏览器缝隙里挖数据
硬件信息是设备指纹的硬核部分。浏览器不会直接告诉你「CPU型号是Intel i7-12700」,但我们可以通过一些间接手段推断。
3.2.1 Canvas指纹
Canvas指纹是我个人觉得最经典的方法。不同设备、不同显卡、不同驱动,渲染同一段文字或图形时,像素输出会有细微差异。这些差异肉眼看不出来,但计算机能精确比对。
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 绘制文字和图形
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('Device Fingerprint', 2, 15);
// 获取像素数据
return canvas.toDataURL();
}
3.2.2 WebGL指纹
WebGL能暴露更底层的GPU信息。我曾在项目中用WebGL指纹区分出同一品牌、同一型号但不同批次的显卡——这个区分度相当惊人。
function getWebGLInfo() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return null;
return {
renderer: gl.getParameter(gl.RENDERER),
vendor: gl.getParameter(gl.VENDOR),
version: gl.getParameter(gl.VERSION),
shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
// 扩展信息
extensions: gl.getSupportedExtensions()
};
}
3.2.3 Audio指纹
音频处理也会因硬件差异产生微小不同。AudioContext处理音频信号时,不同声卡、不同驱动会导致输出波形有细微偏差。
function getAudioFingerprint() {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const analyser = audioCtx.createAnalyser();
oscillator.connect(analyser);
oscillator.start(0);
const buffer = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(buffer);
return Array.from(buffer).join(',');
}
3.3 软件环境探测:浏览器与系统配置
软件环境包括浏览器本身、安装的插件、字体、以及各种配置项。这部分信息变化频率比硬件高,但组合起来也能提供不错的区分度。
3.3.1 字体探测
不同操作系统预装字体不同,用户也可能安装额外字体。通过测量文字宽度可以探测系统安装了哪些字体:
function detectFonts() {
const testFonts = [
'Arial', 'Helvetica', 'Times New Roman', 'Courier New',
'微软雅黑', '宋体', 'SimSun', 'SimHei',
'Roboto', 'Open Sans', 'Noto Sans'
];
const baseWidth = measureTextWidth('mmmmmmmmmmlli', 'monospace');
const installed = [];
testFonts.forEach(font => {
const width = measureTextWidth('mmmmmmmmmmlli', font);
if (width !== baseWidth) {
installed.push(font);
}
});
return installed;
}
3.3.2 插件与扩展探测
通过navigator.plugins可以获取已安装的浏览器插件。不过要注意,现代浏览器(尤其是Chrome)对插件信息做了限制:
function getPlugins() {
const plugins = [];
for (let i = 0; i < navigator.plugins.length; i++) {
const plugin = navigator.plugins[i];
plugins.push({
name: plugin.name,
filename: plugin.filename,
description: plugin.description
});
}
return plugins;
}
3.3.3 浏览器特性检测
不同浏览器对Web标准的支持程度不同。通过检测特定API是否存在,可以推断浏览器类型和版本:
| 检测项 | API/特性 | 区分价值 |
|---|---|---|
| Chrome特有 | chrome.runtime |
高 |
| Edge特有 | msSaveBlob |
中 |
| Safari特有 | safari.pushNotification |
高 |
| Firefox特有 | mozInnerScreenX |
中 |
| WebGL支持 | WebGLRenderingContext |
低(几乎都支持) |
3.4 采集架构总览
下面这张图展示了设备指纹采集的整体流程。我习惯把采集分成三个层级:基础层、硬件层、环境层。每一层采集的数据最终汇聚到指纹生成模块。
3.5 采集注意事项
做了这么多年设备指纹,我总结了几条铁律:
- 不要一次性采集所有数据——浏览器可能会检测到异常行为模式,建议分批次、间隔采集
- 做好降级处理——每个API调用都要try-catch,有些浏览器会直接抛异常
- 数据归一化——同样的信息在不同浏览器返回格式可能不同,比如
navigator.language有的返回zh-CN,有的返回zh - 避免敏感数据——不要采集地理位置、通讯录等需要用户授权才能获取的信息
核心要点: 设备指纹采集不是「越多越好」,而是「越稳定越好」。一个每次访问都变化的数据点,对指纹识别毫无价值。我建议在采集阶段就做稳定性测试——连续采集5次,看哪些字段是稳定的,哪些会波动。只保留稳定字段参与指纹计算。