常见问题
← 返回全部问题

Canvas、WebGL、AudioContext 指纹是怎么被采集的?噪音模式怎么选?

发布时间:2026-05-20 访问量:6250 全文约 5509 字 分类:指纹与检测
回答摘要这三项都不是浏览器主动上报的,而是网页让你的电脑现场画一张图、算一段音频,再看结果和别人差在哪里——差异来自真实的显卡驱动、字体栈和 CPU 浮点运算,所以只改字符串没用。三种处理方式里,默认选「噪音」:让每个窗口有一个自己的固定偏移量。关键不是噪音幅度多大,而是同一个窗口每次打开都要得到同一个值;忽大忽小的噪音本身就是一种特征。

先说结论:这三项和 UserAgent、分辨率那类参数不是一种东西。后者是浏览器主动上报的一行文本,网页问、浏览器答;而 Canvas、WebGL、AudioContext 是网页让你的电脑现场干一件活——画一行字、渲染一个三角形、算一段音频——再看你干出来的结果和别人差在哪。差异来自真实的显卡驱动、字体渲染方式和 CPU 浮点运算细节,光改字符串没用。三种处理方式(噪音、真实值、屏蔽)里默认选噪音;但真正决定成败的不是噪音幅度,而是同一个窗口每次打开是否都得到同一个值

主动上报 vs 被动测量:为什么这三项最不好糊弄

改 UserAgent 是改一句自我介绍,改完对面读到的就是新内容,一次性的事。而 Canvas 这类指纹的采集过程里,网页从来没问过你的硬件型号,它只是给你一段完全相同的绘图指令,然后把你交回来的像素拿去做哈希(把一大段数据压缩成一小串固定长度字符的算法,数据变一点点,结果就完全不同)。显卡驱动、字体文件版本、图形栈实现共同决定了这些像素长什么样。要「改」它就不能只改一句话,得在浏览器内核里拦住绘图结果本身。行业里管这三项叫硬件指纹或被动指纹,参数总清单在《比特浏览器能改哪些浏览器指纹?完整参数清单》里。

Canvas:画一行字,再把像素变成一串哈希

Canvas 是 HTML 里的一块画布元素,本来是给网页画图表、做游戏用的。采集方借用了它,流程固定成四步:

  1. 在页面里建一块看不见的画布,通常只有几百像素大,不插入可视区域,你在屏幕上完全看不到它。
  2. 用一段写死的指令往上画东西:一段大小写混杂、带标点和符号的英文文字,指定一个常见字体,再叠一个色块,然后把同一行文字用半透明颜色错开几个像素再画一遍。
  3. toDataURL() 把这块画布导出成 PNG 图片的 base64 文本,或者用 getImageData() 直接把原始像素数组读出来。
  4. 对这串数据做哈希,得到一个几十位的字符串,这就是 Canvas 指纹。
// 采集侧的典型做法(简化)
const c = document.createElement('canvas');
const g = c.getContext('2d');
g.textBaseline = 'top';
g.font = "14px 'Arial'";
g.fillStyle = '#f60';
g.fillRect(125, 1, 62, 20);
g.fillStyle = '#069';
g.fillText('Hello, canvas 1.0', 2, 15);
g.fillStyle = 'rgba(102, 204, 0, 0.7)';
g.fillText('Hello, canvas 1.0', 4, 17);
const sig = hash(c.toDataURL());   // 这一串就是 Canvas 指纹

第二步里的三个细节都是故意的。用文字,因为文字最能放大差异:指定 Arial 之后,不同系统实际顶上来的字体文件并不一样,字形轮廓、字间距、hinting(字体里控制小字号下笔画如何对齐像素网格的一套数据)都不同。用半透明叠加,因为两层文字错开几像素重叠后,每个边缘像素的颜色都是一次混色计算的结果,而不同实现的混色取舍、伽马校正有微小差别。用抗锯齿边缘,因为把斜边曲线画成方形像素时必须插值,灰度抗锯齿和次像素抗锯齿出来的边缘完全不是一回事。

于是同一段代码在不同电脑上的产出会分叉,分叉点主要有四个:装的是哪个版本的字体文件;系统的光栅化与抗锯齿设置;绘图走硬件加速还是软件渲染、驱动是哪一版;以及 PNG 编码器选了什么压缩策略——哪怕肉眼看两张图一样,编码出来的字节也可能不同。

提示 哈希的放大效应值得记一下:只要一个像素的一个颜色通道差了 1,PNG 字节流就变了,校验值就变了,最终哈希完全变成另一串。这既是采集方能靠它区分设备的原因,也是「加一点点噪音就能改变结果」的原因——好处和坏处来自同一个特性。

WebGL:一条线索是字符串,另一条是图像

WebGL 是浏览器里调用显卡做 3D 渲染的接口,暴露的信息比 Canvas 多一个量级,而且是两条互相独立的线索。第一条是能直接读出来的一大堆值:显卡厂商与型号字符串(通过调试扩展拿到的 unmasked vendor / renderer,能直接读到具体型号名)、支持的扩展列表、上百个数值上限(最大贴图尺寸、各类着色器的最大变量数、视口上限、各颜色通道位深等),以及顶点和片元着色器分别报告的浮点精度。第二条是渲染出来的图像本身:让显卡画一个带贴图和光照的立体图形再导出做哈希,原理和 Canvas 一样,只是这次经手的是完整 3D 管线。

这两条线索合起来构成一个麻烦的约束:它们必须彼此对得上。上百个数值上限是由具体型号、驱动版本和系统图形栈一起定死的,成套变化。如果型号字符串写着一块高端独显,而最大贴图尺寸、扩展列表、着色器精度却是集成显卡的典型值,这个矛盾比老实报真实型号更显眼——真实世界里不存在这种机器。同理,渲染图像由真实驱动产出,想让图像跟着假型号走就得拦截像素输出,而不是换个字符串。这就是「WebGL 不能只改型号名」的技术原因。

AudioContext:算的其实不是声音,是浮点数

音频指纹最容易被误解,很多人以为它在探测声卡。其实整个过程完全不碰扬声器,和你插没插耳机也无关。它用的是 OfflineAudioContext——一个只在内存里离线渲染、不输出到声卡的音频上下文,因此能比实时播放快得多地跑完,全程静音。

// 音频指纹的典型流程(简化)
const ctx = new OfflineAudioContext(1, 5000, 44100);
const osc = ctx.createOscillator();          // 数学生成的振荡器
osc.type = 'triangle'; osc.frequency.value = 1000;
const comp = ctx.createDynamicsCompressor(); // 压缩器:把运算链拉长
osc.connect(comp); comp.connect(ctx.destination);
osc.start(0); ctx.startRendering();
ctx.oncomplete = e => {
  const d = e.renderedBuffer.getChannelData(0);
  let sum = 0;
  for (let i = 0; i < d.length; i++) sum += Math.abs(d[i]);
  // sum 就是音频指纹,一个带十几位小数的数字
};

选振荡器而不是音频文件,是因为振荡器波形纯数学生成,输入完全确定,不依赖任何素材。中间串一个动态压缩器则是为了把运算链拉长——压缩器要处理阈值、拐点、压缩比、启动与释放时间,这些计算把细微差异一路放大。差异从哪来?是 CPU 的浮点运算,不是音频硬件。音频处理全程由 CPU 完成,各浏览器内核虽然同源但多年来各自改动累积,加上不同 CPU 架构启用不同的向量化指令、某些系统还换了不同的快速傅里叶变换实现,浮点误差在长运算链上逐步累加,最终那个总和就分叉了。

它的性格值得注意:区分度不高,但极其稳定。同型号同系统的机器很容易撞出同一个值,单独拿出来定位不了谁;但它跨会话、跨无痕模式基本不变,因此非常适合当交叉验证的锚点——其他参数都换了一套,这个数却和上次一模一样,这本身就是线索。

三者横向对比

项目 采集方式 区分度 稳定性 难点
Canvas 画文字与色块后导出像素做哈希 高,同机同浏览器基本不变 依赖字体栈与光栅化,改字符串无效
WebGL 字符串 直接读取型号、扩展与上百个数值上限 很高 上百个值互相约束,改一处要改一套
WebGL 图像 渲染 3D 场景后导出像素做哈希 很高 像素由真实驱动产出,需拦截输出
AudioContext 离线渲染音频后求采样绝对值之和 很高,跨无痕模式不变 看似不起眼,最适合做交叉验证锚点
图注:同一段采集代码在不同窗口里得到不同哈希,而同一个窗口跨会话必须得到相同哈希(示意图)
图注:同一段采集代码在不同窗口里得到不同哈希,而同一个窗口跨会话必须得到相同哈希(示意图)

三种模式:噪音、真实值、屏蔽

模式 做了什么 适合 代价
噪音(推荐默认) 按窗口的固定种子给结果加极小偏移,让哈希稳定地不同 绝大多数多窗口场景 实现不好时噪音本身会成为特征
真实值 不干预,直接返回本机真实结果 本机只运营一个环境;或页面对绘图精度敏感 同机所有窗口在这几项上完全一致
屏蔽 / 关闭 让接口返回空值或不可用 确实不需要这些功能,且能接受页面异常 「什么都读不到」是更罕见的特征,且可能白屏

为什么默认推荐噪音,而不是看起来更「诚实」的真实值?因为真实值的问题是共享:同一台电脑上的十个窗口都返回真实的 Canvas 哈希、显卡型号、音频数值,这十个窗口在这几项上就是同一个值,等于盖了同一个章。反过来,屏蔽的问题是罕见:真实用户几乎不会去关掉 Canvas 或 WebGL,读不到反而落进一个小得多的群体,不少依赖图形绘制的页面还会直接异常。噪音是三者里唯一能同时做到「各窗口不同」和「看起来正常」的选项。

说句实话:噪音本身也可能被识别出来

这一点决定了你该怎么用这个功能。噪音不是隐身衣,它只是把一个真实值换成另一个稳定的值。业界已知的失效方式主要有三种,都不需要什么高深手段:

  • 能被算掉的噪音:如果噪音是对结果整体乘一个固定倍数,采集方拿一个已知的最简输入跑一遍就能反解出这个倍数再除掉。对音频指纹尤其容易,因为它的输出就是一个数字。
  • 能被平均掉的噪音:如果每次读取都重新随机,采集方就连续采几次取中位数或四舍五入,抖动被抹平,剩下的还是原来那个值。更糟的是「同一会话里读三次得到三个结果」本身在真实浏览器里不会发生,等于主动举手。
  • 过头的噪音:幅度太大时导出图像会出现不该有的杂色、音频数值会落到不合理区间。各项都正常、偏偏 Canvas 哈希落在没见过的区域,一样是异常样本。

所以「好的噪音」有三个特征:每个窗口一个自己的种子;同一个窗口无论读几次、无论今天还是下个月读,结果都一致;幅度小到只改变哈希,不改变图像和数值的合理性。你在客户端里能做的就是开启后不要再去动它——反复切换模式、反复重置指纹,恰好会制造出上面第二种情况。

为什么「同一窗口跨会话稳定」比什么都重要

把视角切到采集方就好理解了,对方手里通常有两类判断:

  • 横向:这两个账号是不是同一台设备?靠的是两边这几项指纹相不相同。这一条要求各窗口之间必须不同。
  • 纵向:这个账号这次登录,还是不是上次那台设备?靠的是这次读到的值和历史记录一致不一致。这一条要求同一个窗口必须长期不变。

两条要求指向同一个技术方案:按窗口固定种子。也正因为纵向这一条存在,「每次打开都重新随机一次指纹」是明确有害的做法——它让每一次登录都像换了台新电脑。正常人的电脑,显卡不会每周换一次,字体不会每天变一批,Canvas 哈希理应几个月都不动。稳定性在这里不是可选项,它就是目标本身。

自己验一遍:三分钟确认稳定性

  1. 打开窗口 A,访问一个能显示 Canvas / WebGL / 音频指纹数值的检测站,把三个值抄下来。
  2. 在同一个窗口里刷新页面,再读一次。三个值应该完全一样——如果变了,说明噪音是逐次随机的,这是要处理的问题。
  3. 关掉窗口 A,重新打开,第三次读。三个值仍然应该和第一次完全一样。这一步验证的是跨会话稳定。
  4. 打开窗口 B,同样读一次。三个值应该和窗口 A 都不一样
  5. 如果某一项在 A 和 B 之间相同,说明这一项没有按窗口隔离,需要回到指纹配置里确认该项模式。
  6. 把 A 的三个值记在窗口备注里。以后换代理、升级客户端之后可以再复核一次。
提示 该用哪些检测站、页面上还要看哪些指标、结果怎么读,属于《怎么验证指纹环境是否正常?用哪些检测网站》。本篇自检只针对一件事:这三项在同一窗口里稳不稳定、在不同窗口之间分不分得开。
注意 不要在已经登录了账号的窗口上中途切换这三项的模式。从真实值切到噪音,或者反过来,会让这个窗口的 Canvas 哈希、显卡型号、音频数值同时变成另一组——在纵向比较里,这等于这台设备的硬件被整体换掉了。要调整就在新建窗口时一次定好。
  • 噪音开得越猛越安全:幅度过大反而制造异常值,合理性比不可预测性重要。
  • 音频指纹区分度低可以不管:它区分度低但极其稳定,正适合做跨会话的锚点校验。
  • 这三项配好就不会被关联:它们只是设备特征的一部分,IP 复用、收款与实名信息、操作习惯、平台自己积累的账号数据都不在浏览器管辖范围内。
提示 本篇描述的采集流程属于公开的通用技术原理,各检测站与风控系统的具体实现不同;客户端提供的模式名称与可调项以「指纹配置」页面实际显示为准。任何指纹方案都不能承诺账号不受限制,涉及平台政策以平台官方最新政策为准。

这些内容不在本篇范围内

完整的参数清单和其他各类参数的推荐策略,看《比特浏览器能改哪些浏览器指纹?完整参数清单》;WebRTC 与真实 IP 泄漏,看《WebRTC 会泄漏真实 IP 吗?三种模式怎么设》;检测站怎么用、各项指标怎么读,看《怎么验证指纹环境是否正常?用哪些检测网站》。本篇只回答这三项硬件指纹的采集原理和模式取舍。

同类问题