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-TWlocalService— 如果語音在本機裝置上處理則為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 結合,它為整體指紋增加了一個額外的獨立維度——以及一次額外的一致性檢查。和大多數指紋訊號面一樣,沒有哪個單一開關能徹底抹去它,只有能降低其突出程度的瀏覽器模式和擴充功能。
推薦閱讀:


