You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Playwright(Node.js)如何随机伪造canvas、audio、font-families生成唯一浏览器指纹

Node.js 实现 Playwright 随机唯一指纹浏览器配置

核心覆盖的指纹随机化维度

  • Canvas 指纹:对Canvas 2D、WebGL的绘制结果叠加随机像素噪声、颜色通道偏移,避免生成固定的画布哈希
  • Audio 音频指纹:对音频上下文生成的频域、时域采样数据叠加微小随机扰动,修改音频输出的特征哈希
  • 字体指纹:随机打乱字体接口返回的字体列表顺序,随机剔除少量非系统默认字体,复现不同设备的字体安装差异
  • 关联特征匹配:同步随机化硬件并发数、设备内存、屏幕分辨率、时区、语言偏好、WebRTC暴露策略等关联参数,避免特征矛盾被反爬识别

前置依赖安装

先安装Node.js版Playwright及对应浏览器内核:

npm install playwright
npx playwright install chromium

完整实现代码

const { chromium } = require('playwright');

// Node层随机工具函数
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const randomFloat = (min, max, precision = 4) => parseFloat((Math.random() * (max - min) + min).toFixed(precision));
const randomPick = (arr) => arr[randomInt(0, arr.length - 1)];

// 可选随机参数池
const TIMEZONE_LIST = ['Asia/Shanghai', 'Asia/Tokyo', 'America/New_York', 'Europe/London', 'Europe/Berlin'];
const LANG_LIST = ['zh-CN', 'en-US', 'ja-JP', 'de-DE', 'en-GB'];
const SCREEN_SIZE_LIST = [[1920, 1080], [2560, 1440], [1366, 768], [1440, 900], [2880, 1800]];

async function createUniqueFingerprintBrowser() {
  // 生成本次实例唯一的随机参数
  const [width, height] = randomPick(SCREEN_SIZE_LIST);
  const hardwareConcurrency = randomInt(2, 16);
  const deviceMemory = randomPick([2, 4, 8, 16, 32]);
  const timezone = randomPick(TIMEZONE_LIST);
  const locale = randomPick(LANG_LIST);
  const canvasNoiseSeed = randomFloat(0.001, 0.01);
  const audioNoiseSeed = randomFloat(0.0001, 0.001);

  // 启动浏览器,配置基础启动参数
  const browser = await chromium.launch({
    headless: false,
    args: [
      '--disable-blink-features=AutomationControlled',
      `--window-size=${width},${height}`
    ]
  });

  const context = await browser.newContext({
    viewport: { width, height },
    screen: { width, height },
    locale,
    timezoneId: timezone
  });

  const page = await context.newPage();

  // 页面加载前注入指纹篡改脚本,确保在页面业务JS执行前完成重写
  await page.addInitScript(({ hardwareConcurrency, deviceMemory, canvasNoiseSeed, audioNoiseSeed }) => {
    // 浏览器上下文内工具函数
    const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
    const shuffleArray = (arr) => [...arr].sort(() => Math.random() - 0.5);

    // 重写硬件相关navigator参数
    Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => hardwareConcurrency });
    Object.defineProperty(navigator, 'deviceMemory', { get: () => deviceMemory });
    Object.defineProperty(navigator, 'webdriver', { get: () => false });

    // 1. 随机化Canvas指纹
    const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
    const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
    const originalGetParam = WebGLRenderingContext.prototype.getParameter;

    const addCanvasNoise = (imageData) => {
      const data = imageData.data;
      for (let i = 0; i < data.length; i += 4) {
        // 对RGBA通道叠加微小随机偏移,不影响视觉效果但会改变画布哈希
        data[i] = Math.max(0, Math.min(255, data[i] + randomInt(-2, 2) * canvasNoiseSeed * 100));
        data[i+1] = Math.max(0, Math.min(255, data[i+1] + randomInt(-2, 2) * canvasNoiseSeed * 100));
        data[i+2] = Math.max(0, Math.min(255, data[i+2] + randomInt(-2, 2) * canvasNoiseSeed * 100));
      }
      return imageData;
    };

    CanvasRenderingContext2D.prototype.getImageData = function(...args) {
      const imageData = originalGetImageData.apply(this, args);
      return addCanvasNoise(imageData);
    };

    HTMLCanvasElement.prototype.toDataURL = function(...args) {
      const ctx = this.getContext('2d');
      if (ctx) {
        const imageData = ctx.getImageData(0, 0, this.width, this.height);
        ctx.putImageData(addCanvasNoise(imageData), 0, 0);
      }
      return originalToDataURL.apply(this, args);
    };

    // WebGL参数加随机扰动
    WebGLRenderingContext.prototype.getParameter = function(param) {
      const result = originalGetParam.apply(this, [param]);
      if (typeof result === 'string') return result + randomInt(0, 100);
      return result;
    };

    // 2. 随机化Audio指纹
    const originalGetChannelData = AudioBuffer.prototype.getChannelData;
    const originalGetFreqData = AnalyserNode.prototype.getByteFrequencyData;

    AudioBuffer.prototype.getChannelData = function(...args) {
      const channelData = originalGetChannelData.apply(this, args);
      for (let i = 0; i < channelData.length; i += randomInt(100, 500)) {
        channelData[i] += (Math.random() - 0.5) * audioNoiseSeed;
      }
      return channelData;
    };

    AnalyserNode.prototype.getByteFrequencyData = function(arr) {
      originalGetFreqData.apply(this, [arr]);
      for (let i = 0; i < arr.length; i += randomInt(50, 200)) {
        arr[i] = Math.max(0, Math.min(255, arr[i] + randomInt(-1, 1)));
      }
    };

    // 3. 随机化字体列表
    const originalQueryFonts = navigator.fonts?.query.bind(navigator.fonts);
    if (originalQueryFonts) {
      navigator.fonts.query = async (...args) => {
        const fonts = await originalQueryFonts(...args);
        const shuffled = shuffleArray(fonts);
        // 随机隐藏0-3个字体,模拟不同设备的字体安装差异
        return shuffled.slice(0, shuffled.length - randomInt(0, 3));
      };
    }
  }, { hardwareConcurrency, deviceMemory, canvasNoiseSeed, audioNoiseSeed });

  // 测试时可自行访问指纹检测站点验证指纹唯一性
  return { browser, context, page };
}

// 调用示例
(async () => {
  const { browser, page } = await createUniqueFingerprintBrowser();
  await page.goto('https://example.com');
  // 写入后续业务逻辑
  // await browser.close();
})();

注意事项

  • 注入脚本必须通过addInitScript方法注入,确保在页面所有资源加载前执行,否则会被指纹检测脚本识别到篡改痕迹
  • 噪声偏移量不要设置过大,否则会导致页面渲染异常、音视频播放出错
  • 每次调用createUniqueFingerprintBrowser都会生成独立的随机参数,不会和历史实例指纹重复,如果需要固定某套指纹,可以把随机生成的参数持久化到本地复用
  • 如果需要覆盖WebGL2场景,可以参照WebGL1的重写逻辑,补充对WebGL2RenderingContext.prototype.getParameter的篡改即可

内容的提问来源于stack exchange,提问作者Titi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 08:42:14