Canvas 指纹是最强大的追踪手段之一:网站让浏览器绘制隐藏图像,再用像素级差异生成稳定的设备标识。了解它的工作原理,以及如何降低被追踪的风险。
Canvas 指纹的原理,是让浏览器在一块不可见的 HTML5 画布上绘制文字与图形,再把渲染出的像素读回来,哈希成一段短小而稳定的标识符。由于每台设备在执行同一套绘制指令时,都会因为 GPU、显卡驱动、操作系统和字体引擎的差异而产生细微不同的结果,这段哈希便成了一个悄无声息、无需任何 Cookie 就能跨站跟随你的签名。本文会比基础介绍走得更深:像素究竟在哪里产生分歧、Canvas 到底贡献了多少唯一性、如何判断自己正在被指纹采集,以及为什么某些常见的防御反而会适得其反。
Canvas 指纹究竟如何工作
这项技术依赖 <canvas> 元素——它本是用 JavaScript 绘制图形的工具。追踪脚本根本不需要把画布显示在屏幕上,它完全存在于内存之中。
几乎所有实现的流程都高度一致:
- 创建一块离屏画布。 脚本创建一块很小的画布(常见宽度为 200 到 300 像素),但从不把它挂到页面上显示出来。
- 绘制一个固定场景。 脚本渲染一段预先设定好的文字,常常混用不同字体、颜色、一两个表情符号以及相互重叠的矩形。表情符号和生僻 Unicode 之所以受青睐,是因为它们会迫使系统字体栈做出渲染决策。
- 把像素读回来。 脚本调用
toDataURL()把画布导出为 Base64 编码的 PNG,或用getImageData()读取原始的 RGBA 字节数组。就指纹采集而言,能派上用场的读回路径只有这两条——不论脚本用什么库实现,最终都会调用其中之一。 - 哈希结果。 这些字节被送入哈希函数(常见的有 FNV 或 MurmurHash),生成一段紧凑的指纹,例如
e3b0c44298fc1c14。
精妙之处在于:指令对所有人都一模一样,但输出却不同。两位运行完全相同脚本的访客可能得到不同的哈希;而同一位访客每次回访都会得到相同的哈希——这正是追踪者想要的。
这块画布从不显示。你看不到任何闪烁、方框或视觉提示。正是这种隐蔽性,让 Canvas 指纹成为静默追踪的常用手段。
为什么像素会因设备而异
如果绘制「Hello」本应是确定性的,那字节为什么会变化?答案是,你系统中的多个层级各自引入了细微而可复现的差异。
GPU 与显卡驱动
抗锯齿、次像素渲染以及曲线的填充方式,都有一部分交给 GPU 处理。英特尔集成显卡、苹果自研 GPU 与英伟达独立显卡,对边缘像素的取整方式各不相同。即便两台机器使用相同型号的 GPU,只要驱动版本不同,结果也可能产生分歧。
字体光栅化
最大的单一影响因素是文字渲染。操作系统的字体引擎——Windows 上的 DirectWrite、macOS 上的 Core Text、Linux 上的 FreeType——决定了如何把字形轮廓转成像素。字体微调(hinting)、伽马校正以及具体的抗锯齿算法,都会随平台和版本而变,因此字母「g」边缘那些灰色像素,往往携带了出乎意料的区分信息。
操作系统与已安装字体
请求的字体是否存在,决定了浏览器是否会替换成后备字体。同一个表情符号,在某个系统上可能渲染成扁平的轮廓,在另一个系统上则是全彩字形。系统的彩色表情集(苹果、Noto、Segoe)尤其会留下强烈的印记。
浏览器引擎与版本
Chromium、Firefox 与 WebKit 各有自己的 Canvas 实现。色彩管理以及 toDataURL() 编码 PNG 数据的方式,都可能随浏览器版本而变——这也是为什么浏览器一升级,你的 Canvas 哈希有时就跟着变了。
Canvas 到底贡献了多少唯一性
Canvas 对追踪者有价值,并不是因为它能单独识别你,而是因为它贡献了相当可观的一份熵——也就是衡量一个信号能把「你是谁」缩小到什么程度的指标。在对真实环境中指纹技术的研究里,Canvas 一再被列为被动网站可读取的单项信号中熵值最高的一档,与完整的已安装字体列表不相上下。衡量这种唯一性最知名的公开测试,是 EFF 的 Cover Your Tracks——不过它给出的分数比看上去更容易被误读,这一点我们在《Cover Your Tracks 唯一性分数说明不了什么》中做了拆解。
话虽如此,Canvas 很少能凭一己之力锁定一个人。许多人共享着常见配置——一台装着默认字体、用 Chrome 的标准 Windows 笔记本,会与数以百万计的人完全重合。它真正的威力体现在组合之中:Canvas 加上 WebGL、屏幕参数、时区和语言,合起来便逼近一个近乎唯一的指纹。至于「需要多少 bit 才能让你变得唯一」背后的具体计算,以及由此得到的匿名集大小,参阅《浏览器指纹熵与匿名集详解》。
Canvas 哈希为何「稳定」却又「不永久」
Canvas 指纹有一种容易让人解释错的独特生命周期:这个哈希在同一台设备的多次会话之间是稳定的,但一旦硬件或软件发生变化就不再稳定。这两个方面都很重要,把它们混为一谈,是描述这项技术时最常见的错误。
在同一台机器上,哈希几乎不会变化。同样的 GPU、同样的驱动版本、同样的操作系统字体光栅化器、同样的浏览器版本,每次都会画出完全相同的像素——于是追踪者在你第一次访问和第五百次访问时看到的是同一个值,完全不需要 Cookie。正是这种持久性,才让 Canvas 一开始就能被当作长期标识符使用。
但这个哈希并非永久不变。渲染链条中的任何一层发生变化,它都会随之改变:
- GPU 驱动更新可能会让抗锯齿或曲线填充的取整方式产生几个亚像素的偏移,这就足以让哈希翻转。
- 操作系统升级往往会带来全新的字体光栅化器(新版 DirectWrite、Core Text 或 FreeType),悄无声息地改变每一个字形的绘制方式。
- 浏览器更新可能会改变
toDataURL()编码 PNG 数据的方式,或者 Canvas 2D 上下文的抗锯齿方式,这与操作系统无关,独立发生。 - 更换硬件(新笔记本、不同的 GPU)会产生一个完全不同的值,与旧值毫无延续性。
这在实践中意味着,Canvas 哈希作为标识符的衰变周期是以月为单位,而非以天为单位——在一套稳定的软硬件配置内,它对追踪单次会话或识别回访者很有用,但并不像硬件序列号那样是一个永久指纹。追踪者的应对办法,是把 Canvas 与更稳定的信号组合使用,而不是单独依赖它,这也正是为什么熵的叠加(参见上文匿名集的链接)比任何单一信号都更重要。
| 属性 | Canvas 指纹 |
|---|---|
| 是否需要存储 | 不需要(无 Cookie、无 localStorage) |
| 清除缓存/Cookie 后是否仍有效 | 是 |
| 隐身/无痕模式下是否仍有效 | 通常是 |
| 用户是否可见 | 否 |
| 典型熵贡献 | 在单项被动信号中处于高位 |
| 清除数据能否破解 | 否 |
| 更换 IP 能否破解 | 否 |
网站如何使用 Canvas 指纹
Canvas 指纹具有双重用途,同一套机制既可用于侵犯隐私,也可用于保护安全:
- 跨站追踪。 嵌入在众多网站中的广告与分析网络读取 Canvas 哈希,即便用户清除了 Cookie,也能认出回访者,并据此构建行为画像。
- 反欺诈与账户安全。 银行和支付机构会标记那些 Canvas 指纹与已知设备突然不符的登录,帮助识别账户盗用。
- 机器人与滥用检测。 无头浏览器和自动化框架产生的 Canvas 输出往往要么过于一致,要么以某些破绽十足的方式渲染,因此指纹有助于把真人和脚本流量区分开——参阅 机器人检测技术,了解这在实践中是如何运作的。
- 限流与防滥用。 服务把指纹当作一个稳定的键,用来限制那些靠不断更换 Cookie 来规避配额的账户。
如何判断自己正在被 Canvas 指纹采集
你看不到那块画布,但可以留意伴随它出现的行为。
留意 API 调用
在一次指纹采集尝试中,JavaScript 会在一块从未被加入可见页面的画布上,先调用 getContext('2d'),接着是 fillText(),最后是 toDataURL() 或 getImageData()。浏览器开发者工具和某些隐私扩展能够把这些调用暴露出来。
一个极简的检测钩子
你可以自己给 Canvas API 加上探针,记录可疑的读取行为:
// 检测那些读取画布像素却什么都不显示的脚本
(function watchCanvasReads() {
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
if (!document.body.contains(this)) {
console.warn('[canvas-fp] toDataURL() on an off-screen canvas', this);
}
return origToDataURL.apply(this, args);
};
CanvasRenderingContext2D.prototype.getImageData = function (...args) {
if (!document.body.contains(this.canvas)) {
console.warn('[canvas-fp] getImageData() on an off-screen canvas', this.canvas);
}
return origGetImageData.apply(this, args);
};
})();
要让它生效,得赶在页面自身的脚本之前运行(用 document_start 时机的用户脚本,或在刷新时粘进控制台),然后在一个看不到任何绘图内容的页面上留意有没有告警。误报是难免的:图片编辑组件、图表库和裁剪工具本来就要读回像素。真正值得警惕的信号,是在一个完全没有图形功能的页面上,某块从未挂到文档里的画布被读回了像素。
如果你更想直接看到自己的指纹,而非阅读代码,最简单的办法是运行 BrowserInsight 的指纹检测工具。它会实时计算你的 Canvas 哈希,把 WebGL 与音频信号一并展示出来,并估算你整体指纹的唯一程度——走的正是追踪脚本会用的那条读回路径,只不过全程在你自己的浏览器里完成,不会上传到任何服务器。
各种防御手段及其取舍
世上没有十全十美的防御,而且——这一点尤为关键——某些热门方案反而会让你更容易被识别。而且没有哪两个浏览器引擎会以相同方式处理 Canvas 读回,「浏览器可能会加噪声」这类笼统说法,恰恰掩盖了你在挑选防御手段时真正需要关心的差异。以下是各家厂商目前实际采用的做法。
趋同(Firefox 的 resistFingerprinting 与 Tor 浏览器)
Firefox 内置的 privacy.resistFingerprinting 选项——正是 Mozilla 官方文档 描述的这套保护模式,也是基于 Firefox 构建的 Tor 浏览器默认启用的模式——走的是趋同路线:Canvas 的读回(toDataURL() 或 getImageData())会直接返回一张纯白空图,除非你在权限提示里明确放行;而 WebGL 的厂商与渲染器字符串、屏幕参数、时区等其他高熵信号,则一律报成所有受保护用户共享的固定通用值。于是脚本拿到的,要么是一个毫无价值的结果,要么是整个受保护群体完全相同的同一个值。融入一个庞大而趋同的人群,是最稳健的防御——但代价是一种高度标准化、有时颇受限制的浏览体验,也正因为这个取舍,Mozilla 把它做成了一个需要手动开启的选项,而非 Firefox 的默认设置。
随机化(Brave 的 farbling)
Brave 走的是另一条路,称为 farbling:正如 Brave 官方文档 所说,它对 Canvas 读回按会话、按 eTLD+1 域名加入微小且确定性的噪声——同一个站点在同一次会话内看到的是一个前后一致的伪造值,但下一次会话或换一个站点就会得到不同的值,从而切断跨站关联。其中微妙的风险在于——「被随机化」本身就是可被检测的——若随机化实现得过于粗糙,反而会增加熵,因为「一台 Canvas 带噪声的浏览器」本身就是一项区分特征。设计良好的 farbling 会把噪声控制得既小、又确定、又合理,以避开这个陷阱。关于噪声设计如何决定随机化有效还是适得其反的深入分析,参阅 Canvas 噪声 vs 真实哈希:随机化为何适得其反。
WebKit/Safari 的追踪防护思路
Safari 完全不依赖针对 Canvas 的专门噪声。WebKit 的 Tracking Prevention 文档 描述了一套建立在智能追踪防护(Intelligent Tracking Prevention)之上的更宏观策略:按顶层站点隔离存储、限制脚本对跨站数据的访问,并把持续性的跨站身份识别整体视为一种需要拦截的滥用模式,而不是逐个 API 地打补丁。Canvas 的读取在 Safari 里依然能正常执行,但 ITP 真正瞄准的是让指纹对追踪者有价值的那部分——跨站关联。
Chrome 与原版 Chromium(没有任何 Canvas 防护)
这一点值得直说,因为它覆盖了大多数读者:Chrome 没有任何针对 Canvas 的防护。toDataURL() 和 getImageData() 交回来的,就是你的 GPU 和字体栈真实渲染出的像素,普通窗口和无痕窗口都一样;那些没有自行改造引擎的 Chromium 系浏览器也是同样情形——上文的 Brave 是少数例外。谷歌在这方面的隐私工作,瞄准的是跨站链路本身(第三方 Cookie 政策、Privacy Sandbox 系列 API),而不是让 Canvas 变得不可读。如果你用的是未经改造的 Chrome,就该把自己的 Canvas 哈希视为完全暴露。
拦截扩展(CanvasBlocker)
像 CanvasBlocker 这样的扩展,可以拦截读回、返回伪造值,或按域名随机化。它给了你精细的控制权,但同样的顾虑依然成立:一个异常或过于激进的伪装会显得格格不入,配置不当的拦截器,可能比它想隐藏的 Canvas 还要醒目。
禁用 JavaScript
通过 NoScript 之类的工具关闭 JavaScript,能彻底击败 Canvas 指纹,因为该 API 根本无法运行。代价相当高昂——大多数现代网站离开它就无法正常使用——因此这只适合对安全极度敏感的场景。
| 防御手段 | 工作原理 | 主要取舍 |
|---|---|---|
| Firefox resistFingerprinting / Tor 浏览器 | 返回统一值或阻断读回 | 体验受限、高度标准化 |
| Brave farbling | 按会话、按站点加噪声以切断关联 | 随机化本身可能被检测 |
| Safari(WebKit ITP) | 阻断跨站关联,而非 API 本身 | Canvas 读取仍会成功,防护范围更窄 |
| Chrome / 原版 Chromium | 没有防护,返回真实像素 | 无任何防护可供配置 |
| CanvasBlocker | 拦截/伪造/随机化读回 | 配置不当会适得其反 |
| 禁用 JavaScript | API 根本不运行 | 大多数网站会无法正常使用 |
常见问题
清除 Cookie 能去掉我的 Canvas 指纹吗?
不能。Canvas 指纹不在你的设备上存储任何东西——它是从你的硬件和软件如何渲染图形中推导出标识符的。清除 Cookie、缓存或站点数据对它毫无影响。
隐身或无痕模式能阻止 Canvas 指纹吗?
通常不能。无痕窗口会隔离 Cookie 和历史记录,但你的 GPU、驱动和字体并未改变,因此 Canvas 哈希通常与普通窗口里的一样。无痕模式对付的是基于存储的追踪,而非指纹。
网站能看出我在用 Canvas 拦截器吗?
有时能。如果你的 Canvas 返回了一个明显伪造、空白或带噪声的值,老练的脚本就能推断出有保护正在生效。这也是为什么趋同(看起来和大家一样)往往比随机化(看起来刻意与众不同)更稳健。
Canvas 指纹和 WebGL 指纹是一回事吗?
二者相关但不相同。Canvas 读取的是文字与图形的 2D 渲染;WebGL 则通过 3D 渲染探测你的 GPU,并直接暴露厂商与渲染器字符串。在一个组合指纹中,二者相辅相成——我们的 WebGL 指纹深度解析 详细讲解了 3D 这一侧。


