speechSynthesis.getVoices() 无需任何权限提示即可暴露操作系统和浏览器已安装的语音合成列表,是一种稳定且很少被屏蔽的指纹信号。
每一个操作系统都自带一套内置的文本转语音(Text-to-Speech)语音库,而每一个浏览器都会把完整列表暴露给任何请求它的网页——不需要点击,不需要权限对话框,也没有摄像头那样的权限提示。window.speechSynthesis.getVoices() 最初是为了让屏幕阅读器和朗读功能能够选择合适的语音而设计的,但它返回的列表同时也是一种悄无声息却相当有效的指纹信号:它同时反映了你的操作系统、语言包和浏览器厂商,而且即便清除 Cookie 也纹丝不动。
关键要点
speechSynthesis.getVoices()无需任何权限,就能返回已安装文本转语音语音的完整列表给任何调用它的脚本。- 每个语音都暴露
name、lang、localService和default字段——而语音的集合、顺序和数量会因操作系统、已安装的语言包和浏览器厂商而异。 - 这份列表通常是异步加载的:首次调用可能返回空数组,脚本必须等待
voiceschanged事件才能看到真实的清单——而这种行为本身在不同浏览器之间也不同。 - 语音清单可以与你所声称的操作系统和浏览器进行交叉核对,就像 GPU 字符串一样——一个"macOS Safari"的 User-Agent 却配上一份仅限 Windows 的语音清单,就是一个值得警惕的不匹配信号。
- 该信号在多次会话之间保持稳定,不受清除 Cookie 或隐私浏览模式的影响,因为它来自已安装的软件,而不是存储的数据。
什么是语音合成指纹?
语音合成指纹读取的是浏览器通过 Web Speech API 的语音合成接口能看到的文本转语音清单,这一 API 家族同时也驱动着屏幕阅读器和"朗读页面"浏览器扩展。调用本身 SpeechSynthesis.getVoices() 是同步的,不需要任何权限,返回一个 SpeechSynthesisVoice 对象数组,描述浏览器能够交给 speak() 使用的每一个语音。
采集过程中不会播放任何声音,也不会产生任何音频——这是一次纯粹的清单读取,其性质更接近字体枚举或媒体设备枚举,而不是测量"渲染方式"的音频或 Canvas 指纹。这里重要的只是安装了哪些可选的具名资源——而这份清单在不同机器之间的差异,已经足以单独构成一个有用信号,叠加其他信号后更是如此。
每个语音暴露了什么
数组中的每一项都是一个 SpeechSynthesisVoice,携带四个可读字段:
name— 人类可读的标签,例如 "Microsoft David - English (United States)" 或 "Samantha"lang— 一个 BCP 47 语言标签,例如en-US或zh-CNlocalService— 如果语音在本地设备上处理则为true,如果依赖远程网络服务则为false(部分 Chrome 内置的 Google 语音属于此类)default— 如果未指定语音时浏览器会选用该语音,则为true
function getVoiceFingerprint() {
const voices = speechSynthesis.getVoices();
return voices
.map(v => `${v.name}|${v.lang}|${v.localService}`)
.sort()
.join(';');
}
脚本根本不需要调用 speak()——读取数组并对排序后的 name/lang/localService 三元组做哈希,就足以为这台机器的语音清单生成一个稳定标识符。
异步加载的坑
追踪者(以及开发者)需要绕开的一个小问题是时机。在部分浏览器上,页面加载后立即调用 getVoices() 会返回空数组,因为浏览器还没来得及向操作系统询问已安装的语音。MDN 文档记录的解决办法,是监听 voiceschanged 事件,在事件触发后重新读取列表:
function collectVoicesReliably() {
return new Promise((resolve) => {
let voices = speechSynthesis.getVoices();
if (voices.length) return resolve(voices);
speechSynthesis.onvoiceschanged = () => {
resolve(speechSynthesis.getVoices());
};
});
}
这个坑不仅仅是开发者的烦恼——它本身也是一种指纹信号。一个浏览器是否需要 voiceschanged 这个变通方案,以及清单填充需要多长时间,会因浏览器引擎和平台而异,在语音清单本身之外又增加了一个小维度。
有多独特、多稳定?
语音清单沿着三个相互独立的轴变化,每一个都会拉开不同画像之间的距离:
| 维度 | 变化原因 |
|---|---|
| 操作系统 | Windows、macOS 和 Linux 各自内置了不同名称的默认语音集 |
| 已安装的语言包 | 配置了多语言环境的机器暴露的语音比单语言默认安装更多 |
| 浏览器厂商 | 部分浏览器会在操作系统语音清单之上叠加自带或依赖网络的语音(localService: false);另一些则只暴露操作系统提供的内容 |
由于这份清单反映的是真实安装的软件,而不是某种渲染上的怪癖,它的行为不太像随机的逐设备噪声源,反而更接近一份结构化清单——比起"这块 GPU 如何对浮点数取整",更接近"安装了哪些字体"这类问题。默认单语言安装会产生一份短小、许多相似机器共有的常见清单;而安装了多个语音包的多语言重度用户配置则会显得突出。结合 WebGL 和已安装字体等信号,它为整体指纹增加了另一个基本独立的维度,遵循我们浏览器指纹完整指南中所讲的同一种组合逻辑:没有任何单一信号是决定性的,但独立信号会相乘放大。
它同时也是一种有用的一致性检查。一份完全由 Windows SAPI 风格名称组成的语音清单,却搭配一个声称是 macOS 的 User-Agent,正是那种会暴露出被伪造或配置错误的画像的矛盾之处,与指纹硬件不匹配一文中提到的 GPU/操作系统不匹配同属一类——语音清单必须与浏览器所报告的一切其他信息保持一致。
从稳定性来看,同一台机器的语音清单很少变化。只有当操作系统更新其内置语音、用户安装或移除语言包,或浏览器重大更新新增了内置语音时,它才会发生变化——除此之外,它是一个持久的跨会话标识符,清除 Cookie、打开隐私窗口或清空缓存都无法将其重置。
防护措施
- 注重隐私的浏览器配置。 以统一性为设计理念的浏览器和模式——Firefox 的指纹防护设置以及 Tor Browser——旨在归一化或限制像这样具有高度设备特异性的 API 表面,代价是在依赖完整语音清单的网站上表现异常。
- 理解隐私浏览模式的局限。 与音频和 Canvas 指纹一样,隐身或隐私模式不会改变已安装的软件,因此也不会改变你的语音清单。
- 阻止或伪造 Web Speech API 的扩展能压平这一信号,但要留意它们不会仅仅返回一个空数组——这种做法本身也会显得异常,从而变得可识别,这正是我们浏览器指纹完整指南中提到的反指纹悖论在这里同样适用。
没有任何一个设置能在不移除你实际使用的语言包,或不采用以融入庞大统一群体为目标的浏览器模式的前提下,把你安装的语音清单缩减为"常见"清单。
如何测试你自己的信号
你无需编写任何代码,就能看到自己的语音指纹:
- 运行一个指纹检测工具。 BrowserInsight 的指纹检测会连同 Canvas、WebGL 和音频一起展示你的浏览器暴露的信号,而 EFF 的 Cover Your Tracks 项目会显示你的整体配置在近期访客中有多罕见。
- 在同一台机器上跨浏览器比较。 在同一台电脑上的两个不同浏览器中打开 MDN 的
getVoices()演示——即便操作系统层面的语音大体重叠,语音的数量以及是否存在依赖网络的(localService: false)条目通常也会不同。 - 在依赖它之前先检查浏览器支持情况。 Web Speech API 语音合成部分的覆盖率虽然广泛,但在较旧的浏览器版本中并非全面支持——可查看 caniuse 了解当前的支持情况。
常见问题
speechSynthesis.getVoices() 需要权限提示吗?
不需要。与摄像头或麦克风访问不同,读取已安装的语音清单不需要任何用户权限,也不会触发任何对话框。任何页面上的任何脚本都可以调用它,并立即读取结果(受上文所述的异步加载问题影响)。
为什么 getVoices() 有时会返回空数组?
部分浏览器会在页面加载后异步从操作系统获取语音清单,因此调用得太早会一无所获。标准的解决办法是监听 voiceschanged 事件,并在事件触发后重新读取列表,如上文的代码示例所示。
语音合成指纹能单独识别我吗?
单独使用时很少能做到——常见的单语言操作系统/浏览器组合会产生许多人共有的语音清单。当它与字体、Canvas 和 WebGL 等其他信号结合,或作为对你所声称操作系统的一致性检查时,才会变得有意义。
清除 Cookie 或使用隐私模式会重置我的语音指纹吗?
不会。语音清单反映的是设备上安装的软件,而不是存储的浏览器数据,因此清除 Cookie、使用隐私窗口或切换配置文件都不会改变它。
结语
语音合成指纹把一个面向无障碍访问的屏幕阅读器 API,变成了一次悄无声息、无需权限的操作系统与浏览器已安装语音清单读取。单独来看,它按平台和语言配置把浏览器粗略分组;与字体、Canvas 和 WebGL 结合,它为整体指纹增加了一个额外的独立维度——以及一次额外的一致性检查。和大多数指纹信号面一样,没有哪个单一开关能彻底抹去它,只有能降低其突出程度的浏览器模式和扩展。
推荐阅读:


