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();
}
注意: Canvas指纹在隐身模式下可能被浏览器拦截或返回空白数据。另外,某些广告拦截插件也会干扰Canvas输出。采集时建议做多次采样取稳定值。

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;
}
经验之谈: 字体探测在移动端效果一般,因为iOS和Android的预装字体高度统一。但在桌面端,尤其是设计师或开发者的电脑上,字体列表能提供很强的区分度。

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 采集架构总览

下面这张图展示了设备指纹采集的整体流程。我习惯把采集分成三个层级:基础层、硬件层、环境层。每一层采集的数据最终汇聚到指纹生成模块。

设备指纹采集架构图 JavaScript API调用 navigator对象 screen对象 时间/时区信息 硬件信息获取 Canvas指纹 WebGL指纹 Audio指纹 软件环境探测 字体探测 插件/扩展探测 浏览器特性检测 数据汇聚与预处理 去重、归一化、异常值过滤 指纹哈希生成 SHA-256 / MurmurHash 设备指纹输出 采集原则 不触发权限弹窗 做异常值过滤

3.5 采集注意事项

做了这么多年设备指纹,我总结了几条铁律:

  • 不要一次性采集所有数据——浏览器可能会检测到异常行为模式,建议分批次、间隔采集
  • 做好降级处理——每个API调用都要try-catch,有些浏览器会直接抛异常
  • 数据归一化——同样的信息在不同浏览器返回格式可能不同,比如navigator.language有的返回zh-CN,有的返回zh
  • 避免敏感数据——不要采集地理位置、通讯录等需要用户授权才能获取的信息

核心要点: 设备指纹采集不是「越多越好」,而是「越稳定越好」。一个每次访问都变化的数据点,对指纹识别毫无价值。我建议在采集阶段就做稳定性测试——连续采集5次,看哪些字段是稳定的,哪些会波动。只保留稳定字段参与指纹计算。