In this Article
WebGL指纹是一种被反机器人系统广泛用于检测自动化活动的识别技术。它利用硬件层面的信号,因此难以绕过,也给依赖网页抓取开展业务的人带来了新的挑战。不过,仍有办法降低被识别的风险并采集数据,继续阅读即可了解具体做法。
什么是WebGL:详解
WebGL是Web Graphics Library的缩写,它是一个JavaScript API。浏览器借助它显示动态2D和3D图形,无需安装插件。为完成这些渲染任务,该API高度依赖GPU,即Graphics Processing Unit(图形处理单元)。如今的网站广泛使用WebGL来实现视觉效果、游戏和复杂的图形界面。事实上,WebGL 2正逐渐成为主流,因为它提供了更多功能。Chrome、Edge和Safari等现代浏览器对WebGL 2的支持率为95-99%,而较旧的浏览器和无头浏览器仍缺乏支持。
市面上的GPU多达数百种,即使同一制造商的不同型号之间也有很大差异,因此同一张图像的渲染结果也会不同。此外,浏览器、已安装的驱动程序,甚至硬件的差异,比如GPU与主板的连接方式,也会影响结果。这就好比走进一家家电商店,看到几十台展出的电视机,全都播放着同一则广告,但画面却略有不同。这正是WebGL指纹的基础。请注意,只有启用JavaScript时才能生成WebGL指纹,静态HTML页面无法执行相关操作。
WebGL指纹:分步概览
大致而言,整个过程如下:
- 网站创建一个<canvas>元素,该元素隐藏在页面的HTML中。它会初始化WebGL渲染环境。
- JavaScript通过调用gl.getParameter()提取GPU特征,例如供应商和型号。
- 指纹脚本让浏览器渲染一张测试图像,并使用readPixels()提取像素数据。
你可以访问 BrowserLeaks,点击WebGL Report,即可查看你的唯一指纹。效果如下:
下面是网站可能使用的脚本示例。你也可以将其用于测试目的,例如诊断、错误报告、兼容性测试等等。请记住,只在你自己拥有的设备上运行该脚本,或者在获得明确同意之后运行。请勿将该脚本用于追踪或识别匿名用户。请将结果视为敏感数据,未经许可不要与第三方分享。DataImpulse对该脚本的非法使用不承担责任。
步骤1:初始化WebGL环境
// Lightweight FNV-1a hash for strings/byte arrays (synchronous, simple)
function fnv1aHashFromBytes(bytes) {
let h = 0x811c9dc5 >>> 0;
for (let i = 0; i < bytes.length; i++) {
h ^= bytes[i];
// multiply by FNV prime (mod 2^32)
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
}
return ('00000000' + (h >>> 0).toString(16)).slice(-8);
}
function fnv1aHashFromString(s) {
const bytes = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i) & 0xff;
return fnv1aHashFromBytes(bytes);
}
// Create an (optionally hidden) canvas and return WebGLRenderingContext / WebGL2RenderingContext
function createWebGLContext(opts = {webgl2Preferred: true, preserveDrawingBuffer: false}) {
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 256;
// optionally keep canvas off-DOM so it stays invisible
canvas.style.display = 'none';
document.body && document.body.appendChild(canvas); // append only if available
let gl = null;
if (opts.webgl2Preferred) {
try { gl = canvas.getContext('webgl2', {preserveDrawingBuffer: opts.preserveDrawingBuffer}); } catch (e) {}
}
if (!gl) {
try { gl = canvas.getContext('webgl', {preserveDrawingBuffer: opts.preserveDrawingBuffer}) || canvas.getContext('experimental-webgl'); } catch (e) {}
}
return {gl, canvas};
}
这段代码会创建一个在后台运行的隐藏画布,并设置其分辨率。此外,若不支持WebGL 2,它会平滑降级为WebGL。
步骤2:获取硬件信息
function getWebGLHardwareInfo(gl) {
const info = {};
// Standard parameters
info.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
info.maxCombinedTextureImageUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
info.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
info.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
info.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
info.shaderPrecision = {
vertexHighFloat: gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT),
fragmentHighFloat: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT)
};
// Try to get unmasked renderer/vendor (if browser exposes WEBGL_debug_renderer_info)
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
if (dbg) {
info.unmaskedVendor = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL);
info.unmaskedRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
} else {
info.unmaskedVendor = null;
info.unmaskedRenderer = null;
}
// Extensions list (useful fingerprinting signal)
info.extensions = gl.getSupportedExtensions() || [];
// Anisotropy (if available)
const extAniso = gl.getExtension('EXT_texture_filter_anisotropic') ||
gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
if (extAniso) {
const maxAniso = gl.getParameter(extAniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
info.maxAnisotropy = maxAniso;
}
return info;
}
它会收集GPU的详细信息,例如供应商、型号、内存大小、最大纹理尺寸、着色器uniform限制、支持的WebGL扩展以及其他数据。
步骤3:检测着色器精度
function probeShaderPrecisions(gl) {
const precisions = {};
const shaderTypes = [
{type: gl.VERTEX_SHADER, name: 'vertex'},
{type: gl.FRAGMENT_SHADER, name: 'fragment'}
];
const levels = [{p: gl.LOW_FLOAT, name: 'low'}, {p: gl.MEDIUM_FLOAT, name: 'medium'}, {p: gl.HIGH_FLOAT, name: 'high'}];
for (const sh of shaderTypes) {
precisions[sh.name] = {};
for (const lv of levels) {
const pf = gl.getShaderPrecisionFormat(sh.type, lv.p);
precisions[sh.name][lv.name] = {rangeMin: pf.rangeMin, rangeMax: pf.rangeMax, precision: pf.precision};
}
}
return precisions;
}
随后,网站会查询GPU对浮点运算和片段着色器的精度支持级别,并记录相应的数值范围。
脚本收集完硬件和软件信息后,会进入下一步:图像渲染。
步骤4:渲染测试场景
// Simple shader pair: position-only vertex shader and color-fragment that depends on coords
const VERT = `#version 300 es
in vec2 a_pos;
out vec2 v_uv;
void main() {
v_uv = a_pos * 0.5 + 0.5;
gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
const FRAG = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outColor;
void main() {
// deterministic function of coordinates that will exercise GPU math
float r = sin(v_uv.x * 123.456) * 0.5 + 0.5;
float g = cos(v_uv.y * 78.9) * 0.5 + 0.5;
float b = fract((v_uv.x + v_uv.y) * 9876.54321);
outColor = vec4(r, g, b, 1.0);
}`;
function createProgramWebGL2(gl, vertSrc, fragSrc) {
function compile(src, type) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
const err = gl.getShaderInfoLog(s);
gl.deleteShader(s);
throw new Error('Shader compile error: ' + err);
}
return s;
}
const vs = compile(vertSrc, gl.VERTEX_SHADER);
const fs = compile(fragSrc, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const err = gl.getProgramInfoLog(program);
throw new Error('Program link error: ' + err);
}
gl.deleteShader(vs);
gl.deleteShader(fs);
return program;
}
function renderTestAndHash(gl, canvas) {
const isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
if (!isWebGL2) {
// For WebGL1 we'd need slightly different shaders (no #version, use attribute/varying)
// but the concept is the same. For clarity this example uses WebGL2 if available.
console.warn('Example uses WebGL2 for deterministic shader pipeline; falling back may require shader edits.');
}
// Create program & buffer
const prog = createProgramWebGL2(gl, VERT, FRAG);
gl.useProgram(prog);
const posLoc = gl.getAttribLocation(prog, 'a_pos');
const quad = new Float32Array([
-1, -1,
1, -1,
-1, 1,
1, 1
]);
const vb = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vb);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
// Render to the canvas
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
const t0 = performance.now();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.finish(); // ensure rendering completes
const t1 = performance.now();
const renderTime = t1 - t0; // render timing in ms
// Read pixels
const px = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, px);
const pixelHash = fnv1aHashFromBytes(px);
// Clean up (optional)
gl.deleteBuffer(vb);
gl.deleteProgram(prog);
return {pixelHash, renderTime};
}
脚本会让浏览器显示一个2D或3D场景,其中可能包含特定的阴影、渐变或效果。随后,它读取像素字节,获取像素颜色、图像纹理、着色和渐变等信息,计算这些数值的哈希,并测量渲染时间。
步骤5:将数据汇总为指纹
function collectWebGLFingerprint(opts = {}) {
const {gl, canvas} = createWebGLContext({webgl2Preferred: true, preserveDrawingBuffer: true});
if (!gl) throw new Error('No WebGL available');
const hw = getWebGLHardwareInfo(gl);
const precisions = probeShaderPrecisions(gl);
// optional: probe more capabilities
const supportsFloatTextures = !!gl.getExtension('OES_texture_float') || !!gl.getExtension('EXT_color_buffer_float');
// Render test and get pixel hash + timing
const {pixelHash, renderTime} = renderTestAndHash(gl, canvas);
// Combine into a fingerprint string
const parts = [
navigator.userAgent || '',
hw.unmaskedVendor || '',
hw.unmaskedRenderer || '',
hw.maxTextureSize,
hw.maxCombinedTextureImageUnits,
hw.maxVertexAttribs,
supportsFloatTextures ? 'floatTex' : '',
hw.extensions ? hw.extensions.join(',') : '',
JSON.stringify(precisions),
pixelHash,
String(Math.round(renderTime)) // include rounded render time
];
const raw = parts.join('|');
const fingerprint = fnv1aHashFromString(raw);
// Return a readable object
return {
fingerprint,
raw,
components: {
ua: navigator.userAgent,
hardware: hw,
precisions,
pixelHash,
renderTime
}
};
}
// Example usage:
try {
const fp = collectWebGLFingerprint();
console.log('WebGL fingerprint:', fp.fingerprint);
console.log('Components:', fp.components);
} catch (e) {
console.error('Could not collect WebGL fingerprint:', e);
}
最后,脚本汇总前述步骤获得的全部数据,将其组合成一个长字符串并再次计算哈希,从而生成更紧凑、更实用的指纹ID。
整个过程在毫秒之内完成,且对你不可见。采集到指纹后,网站会将其存储并与内部数据库比对,或发送给反机器人服务分析。如果某个指纹与已知的机器人或可疑用户匹配,你就有被封禁的风险。
WebGL伪装的挑战
要让流量特征显得自然并不容易,原因如下。
- GPU渲染行为的差异对人眼来说可能不可见,但对网站而言却显而易见,而且它们在不同页面之间保持一致。清除cookie或使用无痕模式都无济于事。
- 渲染依赖物理硬件提供的数据,而这些数据不在JavaScript修改或浏览器扩展能够影响的范围内。
- 你可以在每次加载页面时修改WebGL参数;但如果你声称GPU是Intel,实际渲染表现却像NVIDIA,网站就会发现这种不一致,足以触发反机器人系统。
- 大多数现代检测系统都会捕捉到篡改WebGL API的企图。
这是否意味着WebGL指纹无法绕过?并非如此,有几种技术确实有效。不过,先明确一个前提。
WebGL指纹伪装合法吗?
你有权保护隐私,因此不能一概认为伪装WebGL指纹违法。它也可用于测试。但若借助伪装实施欺诈或违反网站服务条款,则可能违法。你应当始终阅读服务条款,并避免任何可能损害他人隐私的违法行为。 以下信息仅供教育目的提供。DataImpulse不鼓励你采取任何行动,也不对可能的后果承担责任。
伪装WebGL指纹的有效方法
- 无头浏览器配合额外的库
诸如 Playwright 或Selenium之类的流行工具,借助Playwright Stealth或Undetected Chrome Driver等工具,可以略微修改屏幕尺寸等参数。不过,你需要小心,以确保伪造的变量和渲染出的图像彼此匹配。这种方法不适用于大规模项目,也难以抓取采用最新机器人检测算法的网站。WebGL Fingerprint Defender或CanvasBlocker之类的浏览器扩展也是如此。它们拦截WebGL API调用并返回经过篡改的数据,但渲染出的图像仍可能与之不匹配。
- 选择GPU虚拟化
你可以让浏览器呈现的GPU和/或驱动程序看起来不同于真实的物理GPU。你可以采用几种方法,例如,用Switch Shader之类的软件渲染器替换真实的GPU,或使用配备各种GPU驱动程序的虚拟机。不过,如果预算紧张或技术基础不足,采用这些方法前请三思。此外,要扩展抓取项目也颇具挑战。
- 请求静态API
如果你抓取公开数据,且目标网站提供返回JSON的公开或未公开API,这种方法便可行。对于受保护的动态网站,此方法不可行;但在其他情况下值得考虑,因为API端点通常防护较弱。
- 禁用WebGL
禁用WebGL会阻断所有主要的WebGL指纹信号(供应商/渲染器、扩展、精度、像素哈希、渲染时长)。
如何禁用WebGL
Chrome
方法1 – 通过Chrome Flags:
在地址栏中输入 chrome://flags,按Enter键,然后选择 “Disable WebGL” 或 –disable-webgl。将该标志设置为”Disabled”,然后重新启动Chrome以使更改生效。
方法2 – 通过命令行:
右键点击Google Chrome图标,进入”属性”。查找 “目标” 这一行。该行通常以类似 chrome.exe. 的内容结尾。在该行的最末尾,添加一个空格,然后输入 –disable-webgl。点击”应用” > “确定”。
Firefox
在地址栏中输入 about:config 并按Enter键。如果出现警告,接受即可。查找 webgl.disable。双击并将其值设置为true。
Safari
打开Safari偏好设置,进入”高级”选项卡,然后选择”开发“(如果可用)。在实验性功能中找到WebGL via Metal选项。取消勾选该复选框,然后重新启动浏览器。
不过要谨慎,因为禁用WebGL后,许多网站可能无法正常运行。
利用WebGL指纹的漏洞
你还可以利用一些小技巧。例如,脚本常会缓存结果并重复使用,而非从头计算。你可以检测这一行为,然后在缓存结果被复用前改变环境,以避免与先前记录的身份关联,或迫使网站重新计算该值。
检测器:
// Helper: run the fingerprinting routine and return {time, result}
async function runFingerprintTrial(triggerFn) {
const t0 = performance.now();
const result = await triggerFn(); // should return fingerprint/hash if available
const time = performance.now() - t0;
return { time, result };
}
async function detectCachingRobust(triggerFingerprinting, trials = 5) {
const runs = [];
for (let i = 0; i < trials; i++) {
// small delay to reduce micro-scheduling bias
if (i > 0) await new Promise(r => setTimeout(r, 30));
runs.push(await runFingerprintTrial(triggerFingerprinting));
}
// compute median time
const times = runs.map(r => r.time).sort((a,b) => a-b);
const medianTime = times[Math.floor(times.length/2)];
// compare first vs median of later runs
const firstTime = runs[0].time;
const laterMedian = times.slice(1).length ? times.slice(1)[Math.floor((times.length-1)/2)] : medianTime;
// check result stability (if trigger returns a fingerprint/hash)
const results = runs.map(r => r.result);
const allSame = results.every(x => x === results[0]);
// simple heuristic: if first run is much slower *and* results identical, likely cached
const speedRatio = firstTime / (laterMedian || 1);
const likelyCached = (speedRatio > 5) && allSame;
return {
runs,
medianTime,
firstTime,
laterMedian,
speedRatio,
results,
allSame,
likelyCached
};
}
强制使用全新环境:
function newWebGLContext(w=256,h=256) {
const c = document.createElement('canvas');
c.width = w; c.height = h;
const gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl');
return {c, gl};
}
强制重新计算:
localStorage.removeItem('fp_token'); // only if you can identify it
// or flush likely storage (be careful: may break site)
localStorage.clear();
indexedDB.deleteDatabase('site-db-name'); // needs precise names
另一种方法是检测页面何时添加指纹脚本,并在此前运行伪装脚本。不过,这种做法很容易被检测到,也可能干扰页面行为,更适合测试或防御用途。
// Robust script interception (best run at document_start)
(function() {
if (window.__scriptInterceptorInstalled) return;
window.__scriptInterceptorInstalled = true;
// Your spoofing routine — define this to patch WebGL or other APIs.
function injectSpoofing() {
try {
// Example: block debug info (replace with your real routine)
const origGetExt = WebGLRenderingContext.prototype.getExtension;
WebGLRenderingContext.prototype.getExtension = function(name) {
if (name === 'WEBGL_debug_renderer_info') return null;
return origGetExt.call(this, name);
};
// mark that spoofing ran
window.__spoofingInjected = true;
} catch (e) {
// swallow errors — don't break page
console.error('injectSpoofing error', e);
}
}
// Helper: decide whether a script looks like fingerprinting (tweak to match)
function looksLikeFingerprintScript(srcOrContent) {
if (!srcOrContent) return false;
const s = String(srcOrContent).toLowerCase();
return s.includes('fingerprint') || s.includes('fingerprinting') || s.includes('fingerprintjs') || s.includes('fingerprint2');
}
// 1) Hook HTMLScriptElement.prototype.src property (setter)
try {
const proto = HTMLScriptElement.prototype;
const srcDesc = Object.getOwnPropertyDescriptor(proto, 'src');
if (srcDesc && srcDesc.configurable) {
Object.defineProperty(proto, 'src', {
configurable: true,
enumerable: srcDesc.enumerable,
get: function() { return srcDesc.get.call(this); },
set: function(val) {
try {
// If the script's src looks suspicious, inject before setting
if (looksLikeFingerprintScript(val) && !window.__spoofingInjected) {
injectSpoofing();
}
} catch (e) { /* ignore */ }
return srcDesc.set.call(this, val);
}
});
}
} catch (e) {
// non-fatal
}
// 2) Hook setAttribute on script elements (covers element.setAttribute('src', ...))
try {
const origSetAttr = HTMLScriptElement.prototype.setAttribute;
HTMLScriptElement.prototype.setAttribute = function(name, value) {
try {
if ((name + '').toLowerCase() === 'src' && looksLikeFingerprintScript(value) && !window.__spoofingInjected) {
injectSpoofing();
}
} catch (e) {}
return origSetAttr.call(this, name, value);
};
} catch (e) {}
// 3) As a fallback, monitor append/insert to catch scripts that already had src set
try {
const origAppend = Node.prototype.appendChild;
Node.prototype.appendChild = function(node) {
try {
if (node && node.tagName && node.tagName.toLowerCase() === 'script') {
const src = node.src || node.getAttribute && node.getAttribute('src');
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
}
}
} catch (e) {}
return origAppend.call(this, node);
};
const origInsertBefore = Node.prototype.insertBefore;
Node.prototype.insertBefore = function(node, ref) {
try {
if (node && node.tagName && node.tagName.toLowerCase() === 'script') {
const src = node.src || node.getAttribute && node.getAttribute('src');
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
}
}
} catch (e) {}
return origInsertBefore.call(this, node, ref);
};
} catch (e) {}
// 4) Small DOM observer as an extra safety net (lightweight)
try {
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes || []) {
try {
if (n && n.tagName && n.tagName.toLowerCase() === 'script') {
const src = n.src || (n.getAttribute && n.getAttribute('src')) || n.textContent;
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
obs.disconnect();
return;
}
}
} catch (e) {}
}
}
});
obs.observe(document, { childList: true, subtree: true });
} catch (e) {}
// If the page already has script tags at start, scan them now
try {
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
const s = scripts[i];
const src = s.src || s.getAttribute && s.getAttribute('src') || s.textContent;
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
break;
}
}
} catch (e) {}
})();
此外,不同页面的WebGL环境彼此隔离。你可以创建相互隔离的上下文,防止网站跨页面匹配你的指纹。
WebGL与Canvas
WebGL指纹和Canvas指纹常被混为一谈,二者确实密切相关,但并不完全相同。
Canvas指纹会分析浏览器如何通过HTML5 Canvas渲染2D图形。渲染结果主要取决于浏览器的图形栈和渲染引擎。因此,它更容易被伪装。
WebGL指纹会分析浏览器如何渲染2D和3D图形。渲染结果取决于硬件特性,因此该方法可提供更多信息,也更难绕过。
总结
了解WebGL指纹伪装的挑战后,你或许会觉得十分钟前一无所知时反而更轻松。然而,你的目标并不是隐身,而是让自己看起来像一个”普通用户”。为此,你的指纹需要真实可信,在同一会话内保持一致,并在不同会话之间有所变化。这完全可以做到。另一方面,只有从各个层面维持匿名性,并结合运用多账号浏览器, TLS伪装解决方案, 代理等工具,才能成功进行网页抓取。这些工具和方法相辅相成,可降低被发现的风险。DataImpulse提供有助于保护隐私和匿名性的方案:合法获取且已列入白名单的代理,可满足各种抓取需求,并提供7天24小时人工支持,让你可以持续采集数据。点击“立即试用”,或通过[email protected]联系我们即可开始。

