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
相关产品推荐
相关产品推荐

