navigator.mediaDevices.enumerateDevices() 在授予摄像头/麦克风权限前暴露设备数量、ID 和标签,追踪者借此构建指纹。
每一台内置摄像头和麦克风的笔记本电脑、每一台插着 USB 耳机的台式机、每一部拥有双摄像头的手机——这一切都能通过一次不起眼的 API 调用被网页看到。navigator.mediaDevices.enumerateDevices() 最初的设计目的,是让网站在发起视频通话前列出可用的摄像头和麦克风。它确实做到了这一点,但同时也交出了一个完全无需权限提示的指纹信号,而一旦用户真的授予了摄像头或麦克风权限,这个信号会变得更加丰富。
关键要点
enumerateDevices()无需任何权限提示即可运行,任何调用它的页面都能获取已连接音频/视频设备的数量和类型。- 在权限授予之前,
deviceId和label为空,但设备的数量和类型(有多少摄像头、麦克风、扬声器)依然增加了信息熵。 - 权限授予之后(哪怕只对任意一个网站授予过一次),
deviceId、groupId以及人类可读的label字符串——往往包含硬件型号名称——就会变得可见。 groupId会关联共享同一物理硬件的设备,让脚本能够推断出,例如某个摄像头和麦克风来自同一台网络摄像头设备。- 仅设备数量本身就是一个有意义的跨站信号:大多数桌面设备报告的设备集合较小且稳定,会在多次浏览会话乃至浏览器重启之间保持不变。
enumerateDevices() 返回什么
MediaDevices.enumerateDevices() 方法是 Media Capture and Streams 规范的一部分,它返回一个 Promise,解析为一个 MediaDeviceInfo 对象数组——对应操作系统向浏览器暴露的每一个音频输入、音频输出和视频输入设备。每个对象携带四个字段:
kind—"audioinput"、"audiooutput"或"videoinput"deviceId— 一个不透明的、限定于来源站点的设备标识符groupId— 一个由同一物理硬件的多个设备共享的不透明标识符label— 一个人类可读的设备名称,例如 "FaceTime HD Camera" 或 "Logitech BRIO"
关键在于,这次调用本身不需要摄像头或麦克风权限——只有 getUserMedia() 才需要。页面可以在加载时、在用户与任何内容交互之前就调用 enumerateDevices(),立即得知这台机器上有多少音频和视频设备。
权限授予前后的差异
规范刻意将 deviceId、groupId 和 label 置空,直到调用来源已至少被授予过一次摄像头或麦克风访问权限——这是浏览器厂商对指纹风险做出的让步。但这种置空是部分而非完全的,而这两种状态之间的差异本身就具有信息量。
async function getMediaDeviceFingerprint() {
const devices = await navigator.mediaDevices.enumerateDevices();
const counts = { audioinput: 0, audiooutput: 0, videoinput: 0 };
const labeled = [];
for (const d of devices) {
counts[d.kind]++;
if (d.label) {
// 只有在该来源至少被授予过一次 getUserMedia() 权限后才会填充
labeled.push({ kind: d.kind, groupId: d.groupId, label: d.label });
}
}
return { counts, total: devices.length, labeled };
}
权限授予之前:每个 deviceId 和 label 都是空字符串,但 devices.length 以及按 kind 分类的数量是精确的。一台内置一个麦克风、一个摄像头,并配对了蓝牙耳机的机器,与一台什么外接设备都没有的裸机笔记本,报告的结果不同——而这个数量在用户访问的每一个网站上都保持稳定,构成一个低基数但持久的跨站信号。
权限授予之后:标签和 ID 会立即填充,而且往往泄露的信息不止是一个通用名称。厂商和型号字符串("Logitech BRIO"、"iPhone Microphone")很常见,deviceId 值在该来源站点上会跨会话保持稳定——实际上成为一个限定于该站点的半持久标识符,其原理类似于持久访客 ID如何借助其他稳定信号在清除缓存后依然存活。
groupId:关联物理硬件
groupId 的存在,是为了让应用能够避免选中来自同一物理设备、可能造成回声反馈的麦克风和扬声器组合——但它同时也充当了一个关联键。两个共享同一 groupId 的 MediaDeviceInfo 条目,必然来自同一件硬件,这让脚本无需读取任何型号字符串,就能推断出例如"这个摄像头和这个麦克风是同一台 USB 网络摄像头"这样的信息。结合设备数量和任何可用的标签,groupId 通过排除物理上不可能同时出现的设备组合,进一步收紧了指纹。
设备数量作为跨站信号
在众多指纹采集技术中,设备枚举颇为特殊:它不需要渲染管线、不需要 GPU、也不需要计时测量——它是对物理硬件清单的直接、结构化读取。结合诸如 WebGL 和 Canvas 输出等信号,媒体设备数量增加了另一个独立维度:两个 GPU 相同、Canvas 哈希也相同的访客,仍然可以被区分开——如果其中一个连接了外接显示器扬声器和一个内置麦克风阵列的摄像头,而另一个没有。这与我们浏览器指纹完整指南中所讲的组合原理一致——没有任何单一信号是决定性的,但独立信号会相乘放大。
它还与 WebRTC 存在一种值得特别指出的关联:已经为合法功能(视频聊天、语音留言)请求摄像头/麦克风权限的网站,会"免费"获得完整的带标签设备列表,使一次功能性的权限授予,变成一份持久且异常详细的指纹贡献。
防护措施
- 默认拒绝摄像头/麦克风权限,只在确实打算使用该功能的站点上按需授予——这样能让不需要该权限的站点上
deviceId和label始终为空。 - Firefox 的
media.navigator.enabled及相关首选项,以及基于统一性理念构建的隐私浏览器(Tor Browser),会限制或完全阻止大多数来源站点的枚举,代价是破坏合法的 WebRTC 通话功能。 - 浏览器权限管理器——审查并撤销陈旧的摄像头/麦克风授权(
chrome://settings/content/camera以及 Firefox/Safari 中的对应设置)能缩小可以看到带标签设备的来源站点集合。 - 拔掉不使用的外设能减少设备数量带来的信息熵,但对大多数人来说并不现实,而且只能封堵众多维度中的一个。
以上措施都无法完全消除权限授予前的设备数量信号,因为直接屏蔽 enumerateDevices() 会破坏任何需要提供设备选择器的网站——现实可行的目标是限制有多少来源站点能够到达信息更丰富的权限授予后状态。
想知道你自己的浏览器已经暴露了哪些指纹信号——Canvas、WebGL、字体等等——可以运行 BrowserInsight 的指纹检测,衡量这些信号让你有多容易被识别。
常见问题
enumerateDevices() 需要摄像头或麦克风权限吗?
不需要。调用本身无需任何提示即可工作,并立即返回设备数量和类型。只有 deviceId、groupId 和 label 字段需要该来源站点事先获得 getUserMedia() 权限才会被填充。
网站能在不询问的情况下知道我有多少摄像头或麦克风吗?
可以。devices.length 以及按 kind 分类的明细,在任何权限对话框出现之前就已可用,而且它们会在多次访问之间保持不变,因为它们反映的是连接的硬件,而不是用户清除 Cookie 就能重置的东西。
清除 Cookie 会重置我的媒体设备指纹吗?
不会。设备数量,以及一旦获得授权后的 deviceId/groupId 值,都源自硬件和按来源划分的权限状态,而不是清除 Cookie 会移除的存储数据——这与浏览器指纹完整指南中 Canvas 和 WebGL 指纹在清除缓存后依然存活的原理类似。
媒体设备指纹已经在真实网站上使用了吗?
指纹采集库通常会将设备数量作为众多信号之一纳入其中,而任何拥有合法视频通话或语音录制功能的网站,一旦获得权限,就已经拥有了完整的带标签列表,无论它是否将这些数据用于追踪。


