网站到底是如何通过JS读取你的浏览器指纹的?

在反爬虫与风控的对抗中,爬虫工程师常有一种错觉:我用了最新版的 Chrome,代理 IP 也很干净,为什么一访问就被拦截?原因在于,你的浏览器在打开网页的那一瞬间,就已经在风控的审视下"裸奔"了。

风控系统不需要你输入身份证号,它只通过一段几十 KB 的 JavaScript 代码,就能在毫秒级内给你的浏览器画出一幅精确到像素、时区和硬件毛细血管的"数字画像"。

本文将拆解网站是如何一步步读取你的浏览器指纹的。只有极致了解对方如何侦察,才能在指纹浏览器开发中做到精准反制。

一、 指纹采集的先锋:基础环境与系统特征

风控 JS 下发的第一波请求,是不需要任何计算复杂度的"明文特征",它们挂在 navigatorscreen 对象上,是一切指纹关联的基石。

1. UserAgent 与高熵特征

不仅读取 navigator.userAgent 字符串,现代风控更看重 navigator.userAgentData(Chrome 90+ 引入的 Client Hints API)。

javascript 复制代码
// 风控代码示例:获取高熵特征,剥离伪造的 UA
navigator.userAgentData.getHighEntropyValues([
  "platform", "platformVersion", "architecture", "model", "uaFullVersion"
]).then(ua => {
  // 这里能拿到真实的操作系统架构(x86/arm)、Windows具体版本号
  // 如果你用JS强行改了UA字符串,但没改底层Client Hints,瞬间暴露
});

2. 硬件并发与内存

这是判断设备算力的重要指标,也是区分服务器/模拟器与真实PC的关键。

javascript 复制代码
let cores = navigator.hardwareConcurrency || 0; // CPU逻辑核心数
let memory = navigator.deviceMemory || 0;       // 设备内存GB(受隐私策略限制,可能返回近似值)
// 真实用户:cores通常是4,8,16;memory是4,8,16
// 服务器/爬虫:cores动辄64,memory是32+,或者返回undefined(无头浏览器常见)

3. 屏幕与色彩深度

屏幕分辨率本身容易伪造,但风控看的是"组合逻辑"。

javascript 复制代码
let screenInfo = {
  w: screen.width,           // 屏幕物理宽度
  h: screen.height,          // 屏幕物理高度
  aw: screen.availWidth,     // 可用宽度(去除任务栏)
  ah: screen.availHeight,    // 可用高度
  cd: screen.colorDepth,     // 色深,通常为24或32
  dpr: window.devicePixelRatio // 设备像素比,Retina屏为2或3
};
// 矛盾检测:如果 screen.width=1920, 但 availHeight=1080(没有给任务栏留空间),大概率是虚拟屏幕
// 矛盾检测:如果 dpr=1,但声称是 MacBook Retina 设备,直接判定伪造

4. 时区与语言环境

这是风控进行"时空一致性"校验的起点,与代理 IP 的地理位置强绑定。

javascript 复制代码
let timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // 如 "Asia/Shanghai"
let locale = navigator.language; // 如 "zh-CN"
// 致命矛盾:IP归属地是美国,timezone却是Asia/Shanghai,语言是zh-CN,90%概率是爬虫

二、 深入硬件:高级渲染指纹

基础特征容易伪装,但 GPU 和声卡在执行渲染和计算时的物理微小差异,是无法通过简单修改 JS 变量伪造的。这是风控最核心的"杀招"。

1. Canvas 指纹:GPU 的独特笔迹

Canvas 指纹的原理是:让浏览器在画布上绘制特定图形和文字,由于 GPU 型号、驱动版本、底层渲染引擎(如 Skia/DirectWrite)的抗锯齿算法不同,最终生成的像素数据存在微小差异。

风控实战代码解析:

javascript 复制代码
function getCanvasFingerprint() {
  let canvas = document.createElement('canvas');
  let ctx = canvas.getContext('2d');
  
  // 绘制文本:不同系统的字体渲染引擎(Mac/CoreText vs Win/GDI)差异极大
  ctx.textBaseline = "top";
  ctx.font = "14px 'Arial'";
  ctx.fillStyle = "#f60";
  ctx.fillRect(125, 1, 62, 20);
  ctx.fillStyle = "#069";
  ctx.fillText("Hello, world! 🤖", 2, 15); // 加入Emoji,测试色彩支持
  
  // 绘制图形:测试混合模式与抗锯齿
  ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
  ctx.beginPath();
  ctx.arc(50, 50, 50, 0, Math.PI * 2, true);
  ctx.fill();
  
  // 提取像素数据并计算哈希
  let dataStr = canvas.toDataURL();
  // 风控不会把几MB的图片传回服务器,而是在本地计算哈希
  let hash = murmurhash3(dataStr); 
  return hash;
}

反制难点 :如果你用 JS Hook 拦截 toDataURL 返回一个假哈希,风控可以通过读取 getImageData 检查特定像素点的 RGBA 值是否与哈希匹配;如果你只改结果不改渲染,风控可以测量绘制操作耗费的时间(真实 GPU 绘制耗时与 CPU 模拟耗时差异巨大)。真正的指纹浏览器必须在 C++ 层(Skia 引擎)对输出的像素矩阵注入合法噪声。

2. WebGL 指纹:显卡的身份证

WebGL 提供了比 Canvas 更底层的 GPU 交互接口。风控不仅看渲染结果,更看 GPU 的身份参数。

javascript 复制代码
function getWebGLFingerprint() {
  let canvas = document.createElement('canvas');
  let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
  
  let vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL); // 显卡厂商
  let renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); // 显卡型号
  
  // 典型的暴露特征:
  // VMware/Gallium 3D -> 虚拟机
  // SwiftShader -> 无头模式/软件渲染
  // ANGLE (Intel/NVIDIA...) -> 真实PC
  
  // 此外,风控还会枚举WebGL支持的所有扩展列表(gl.getSupportedExtensions)
  // 不同的GPU和驱动支持的标准扩展集合不同,这又是一个高维特征
}

3. AudioContext 指纹:声卡的回声

不播放任何声音,通过让浏览器处理音频信号,捕捉底层音频硬件和驱动处理的微小差异。

javascript 复制代码
function getAudioFingerprint() {
  let context = new (window.AudioContext || window.webkitAudioContext)();
  let oscillator = context.createOscillator();
  let analyser = context.createAnalyser();
  let gain = context.createGain();
  let scriptProcessor = context.createScriptProcessor(4096, 1, 1);
  oscillator.type = 'triangle';
  oscillator.frequency.setValueAtTime(10000, context.currentTime);
  gain.gain.setValueAtTime(0, context.currentTime); // 静音,用户听不见
  
  // 连接节点并渲染
  oscillator.connect(gain);
  gain.connect(analyser);
  analyser.connect(scriptProcessor);
  scriptProcessor.connect(context.destination);
  
  // 获取处理后的音频波形数据并计算哈希
  // 不同声卡对浮点数的计算精度、压缩算法不同,导致最终波形数据的极微小差异
}

三、 系统级探测:字体与多媒体

1. 字体枚举指纹

系统安装的字体列表是高度个性化的,尤其是中文字体。风控通过测量特定字符在不同预设字体下的渲染宽度,来判断该字体是否存在。

javascript 复制代码
function detectFonts(fontList) {
  let baseFonts = ['monospace', 'sans-serif', 'serif'];
  let testString = "mmmmmmmmmmlli";
  let testSize = '72px';
  let h = document.getElementsByTagName("body")[0];
  let s = document.createElement("span");
  s.style.fontSize = testSize;
  s.innerHTML = testString;
  let defaultWidths = {};
  
  // 获取基础字体的宽度
  for (let font of baseFonts) {
    s.style.fontFamily = font;
    h.appendChild(s);
    defaultWidths[font] = s.offsetWidth;
    h.removeChild(s);
  }
  let detectedFonts = [];
  for (let font of fontList) {
    let found = false;
    for (let baseFont of baseFonts) {
      s.style.fontFamily = `'${font}', ${baseFont}`;
      h.appendChild(s);
      // 如果宽度与基础字体不同,说明该字体被系统成功加载并渲染
      if (s.offsetWidth !== defaultWidths[baseFont]) {
        found = true;
        break;
      }
      h.removeChild(s);
    }
    if (found) detectedFonts.push(font);
  }
  return detectedFonts;
}
// 矛盾检测:声称是 Mac OS,却检测出微软雅黑和宋体,直接封杀

2. 媒体设备探测

摄像头、麦克风的存在与否及数量,也是环境真实性的佐证。

javascript 复制代码
navigator.mediaDevices.enumerateDevices().then(devices => {
  let audioOutput = devices.filter(d => d.kind === 'audiooutput').length;
  let audioInput = devices.filter(d => d.kind === 'audioinput').length;
  let videoInput = devices.filter(d => d.kind === 'videoinput').length;
  // 无头浏览器或服务器通常返回 0 个设备
});

四、 反侦察与对抗:风控如何识破 JS Hook

爬虫工程师常说:"我用了 Vue/React 的 Proxy,把 navigatorCanvas 全拦截了,为什么还是被查?"

因为现代风控拥有极其变态的"反反指纹"机制,专门检测 JS 运行环境是否被污染。

1. 属性描述符检测

原生对象的属性是内置的,其特性与通过 Object.defineProperty 劫持的属性完全不同。

javascript 复制代码
// 爬虫常见的Hook:
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
// 风控反制检测:
let descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
if (descriptor && descriptor.get && descriptor.get.name !== 'get webdriver') {
  // 原生的 getter 名称是 'get webdriver',如果你Hook了,name会变成 'get'
  // 或者 descriptor.configurable 变成了 true(原生一般是 false)
  return "环境被篡改!";
}

2. 原型链与 toString 检测

原生函数(如 toDataURL)转为字符串时,应输出 [native code]

javascript 复制代码
// 爬虫覆盖了 canvas.toDataURL
canvas.toDataURL = function() { return fakeHash; };
// 风控检测
if (canvas.toDataURL.toString().indexOf('[native code]') === -1) {
  return "函数被覆写!";
}
// 更狠的检测:直接去 iframe 中获取纯净的函数来对比

3. 降维隔离检测

风控 JS 会创建一个隐藏的、脱离当前文档流的 <iframe>,获取 iframe 内部的 navigatorcanvas 等对象,与当前页面的对象进行对比。

javascript 复制代码
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
let realNavigator = iframe.contentWindow.navigator;
// 如果当前页面的 navigator.webdriver !== realNavigator.webdriver
// 说明你在当前页面做了全局 Hook,但 iframe 里的原生对象出卖了你

4. CDP 与 WebDriver 铁证

这是 Selenium/Playwright 无法根治的绝症。

javascript 复制代码
// 检测1:最基础的 webdriver 标志
navigator.webdriver === true; // CDP控制下默认为true
// 检测2:Chrome 特有变量
window.cdc_adoQpoasnfa76pfcZLmcfl_Array; // Chromedriver 注入的变量特征
// 检测3:执行堆栈分析
// 风控触发一个错误,检查 Error().stack 
// 如果堆栈中出现 'puppeteer'、'evaluateOnNewDocument' 等字眼,直接击毙

结语 :通过上述拆解,我们可以得出一个残酷的结论:在 JS 层面进行的任何指纹伪装,都是掩耳盗铃。

无论你的 Proxy 写得多优雅,风控总能通过原型链、属性描述符、iframe 隔离和堆栈分析将你扒得底裤不剩。

传统爬虫在 JS 层的 Hook 对抗,已经走进了死胡同。这也正是指纹浏览器必须存在的根本原因。

真正的指纹浏览器,绝不依赖油猴脚本或 JS 注入。它的战场在 C++ 层:

  • Canvas/WebGL 伪装:修改 Chromium 的 Skia/ANGLE 引擎,在 GPU 计算完成后、返回给 JS 引擎之前,在内存中直接对像素矩阵注入合法噪声。
  • 属性篡改 :直接修改 Blink 引擎的 Navigator.idl 和相关 C++ 类的返回值,让 navigator.webdriver 从编译层面就不存在,属性描述符依然是原生的。
  • 环境隔离:利用操作系统的沙箱和命名空间,让每个浏览器实例拥有独立且真实的设备映射。

在风控之眼面前,JS 层的伪装如同纸糊的面具;只有深入内核,重塑骨骼,才能在风控的凝视下隐入尘烟。这也是我们接下来探讨指纹浏览器底层开发的核心方向。

相关推荐
c238561 小时前
C++的IO流深入理解(上)
开发语言·c++
用户938515635071 小时前
从JS的“坑”到TS的“墙”,再到Bun与AI:打造健壮的全栈应用
前端·javascript
SilentSamsara1 小时前
DuckDB + Python:嵌入式 OLAP 数据库的轻量分析实战
开发语言·数据库·python·微服务
橘子星1 小时前
浅谈 TypeScript 与 Bun:现代 JavaScript 开发的利器
前端·javascript
铁皮饭盒1 小时前
Bun 的三种并发"暗器":reusePort、Worker、spawn,能硬刚 Java 吗?
前端·javascript·后端
无限进步_1 小时前
【Linux】进程状态、僵尸与孤儿、进程调度
linux·运维·服务器·开发语言·数据结构·算法
仙俊红1 小时前
反射到底解决什么问题?
java·开发语言
大阳1231 小时前
ARM.9(RGBLCD,PWM)
c语言·开发语言·汇编·单片机·嵌入式硬件·pwm·rgblcd
珊瑚里的鱼2 小时前
C++14 和 C++17 的核心新特性
开发语言·c++