Canvas 指紋是最強大的追蹤手段之一:網站讓瀏覽器繪製隱藏圖像,再用像素級差異生成穩定的裝置標識。了解它的運作原理,以及如何降低被追蹤的風險。
Canvas 指紋的原理,是讓瀏覽器在一塊看不見的 HTML5 畫布上繪製文字與圖形,再把算繪出的像素讀回來,雜湊成一段短小而穩定的識別碼。由於每一台裝置在執行同一套繪製指令時,都會因為 GPU、顯示卡驅動程式、作業系統和字型引擎的差異而產生略有不同的結果,這段雜湊便成了一個悄無聲息、不需任何 Cookie 就能跨站跟著你的簽章。本文會比基礎介紹走得更深:像素究竟在哪裡產生分歧、Canvas 到底貢獻了多少獨特性、如何判斷自己正在被指紋採集,以及為什麼某些常見的防禦反而會弄巧成拙。
Canvas 指紋究竟如何運作
這項技術仰賴 <canvas> 元素——它原本是用 JavaScript 繪製圖形的工具。追蹤指令碼根本不需要把畫布顯示在螢幕上,它完全存在於記憶體之中。
幾乎所有實作的流程都高度一致:
- 建立一塊離螢幕畫布。 指令碼建立一塊很小的畫布(常見寬度為 200 到 300 像素),但從不把它掛到頁面上顯示出來。
- 繪製一個固定場景。 指令碼算繪一段預先設定好的文字,常常混用不同字型、顏色、一兩個表情符號以及彼此重疊的矩形。表情符號和冷僻 Unicode 之所以受青睞,是因為它們會迫使系統字型堆疊做出算繪決策。
- 把像素讀回來。 指令碼呼叫
toDataURL()把畫布匯出為 Base64 編碼的 PNG,或用getImageData()讀取原始的 RGBA 位元組陣列。就指紋採集而言,這是僅有的兩條讀回路徑——不論用什麼函式庫,每一支 Canvas 指紋採集指令碼,最終都會呼叫到其中之一。 - 雜湊結果。 這些位元組被送入雜湊函式(常見的有 FNV 或 MurmurHash),產生一段精簡的指紋,例如
e3b0c44298fc1c14。
精妙之處在於:指令對所有人都一模一樣,但輸出卻不同。兩位執行完全相同指令碼的訪客可能得到不同的雜湊;而同一位訪客每次回訪都會得到相同的雜湊——這正是追蹤者想要的。
這塊畫布從不顯示。你看不到任何閃爍、方框或視覺提示。正是這種隱蔽性,讓 Canvas 指紋成為靜默追蹤的常用手段。
為什麼像素會因裝置而異
如果繪製「Hello」本應是確定性的,那位元組為什麼會變化?答案是,你系統中的多個層級各自引入了細微而可重現的差異。
GPU 與顯示卡驅動程式
反鋸齒、次像素算繪以及曲線的填滿方式,都有一部分交給 GPU 處理。Intel 內顯、Apple 自研 GPU 與 NVIDIA 獨立顯示卡,對邊緣像素的取整方式各不相同。即便兩台機器使用相同型號的 GPU,只要驅動程式版本不同,結果也可能產生分歧。
字型點陣化
最大的單一影響因素是文字算繪。作業系統的字型引擎——Windows 上的 DirectWrite、macOS 上的 Core Text、Linux 上的 FreeType——決定了如何把字形外框轉成像素。字型微調(hinting)、伽瑪校正以及具體的反鋸齒演算法,都會隨平台和版本而變,因此字母「g」邊緣那些灰色像素,往往攜帶了出乎意料的區分資訊。
作業系統與已安裝字型
請求的字型是否存在,決定了瀏覽器是否會替換成後備字型。同一個表情符號,在某個系統上可能算繪成扁平的外框,在另一個系統上則是全彩字形。系統的彩色表情集(Apple、Noto、Segoe)尤其會留下強烈的印記。
瀏覽器引擎與版本
Chromium、Firefox 與 WebKit 各有自己的 Canvas 實作。色彩管理以及 toDataURL() 編碼 PNG 資料的方式,都可能隨瀏覽器版本而變——這也是為什麼瀏覽器一更新,你的 Canvas 雜湊有時就跟著變了。
Canvas 到底貢獻了多少獨特性
Canvas 對追蹤者有價值,並不是因為它能單獨識別你,而是因為它貢獻了相當可觀的一份熵——也就是衡量一個訊號能把「你是誰」縮小到什麼程度的指標。在對真實環境中指紋技術的研究裡,Canvas 一再被列為被動網站可讀取的單項訊號中熵值最高的一檔,與完整的已安裝字型清單不相上下。衡量這種獨特性最知名的公開測試,是 EFF 的 Cover Your Tracks——不過它給出的分數比表面上更容易被誤讀,這一點我們在《Cover Your Tracks 唯一性分數說明不了什麼》中做了拆解。
話雖如此,Canvas 很少能憑一己之力鎖定一個人。許多人共享著常見組態——一台裝著預設字型、用 Chrome 的標準 Windows 筆電,會與數以百萬計的人完全重合。它真正的威力體現在組合之中:Canvas 加上 WebGL、螢幕參數、時區和語言,合起來便逼近一個近乎獨一無二的指紋。關於「究竟要累積多少位元才會變得獨一無二」背後的實際運算,以及最終的匿名集會是什麼樣子,請參閱瀏覽器指紋熵與匿名集詳解。
為什麼 Canvas 雜湊是穩定的——直到它不再穩定
Canvas 指紋有一種容易讓人誤解的獨特生命週期:雜湊在同一裝置的多次工作階段之間是穩定的,但在硬體或軟體發生變化後就不再穩定。這兩個面向缺一不可,把它們混為一談,是描述這項技術時最常見的錯誤。
在同一台機器上,雜湊幾乎不會改變。相同的 GPU、相同的驅動程式版本、相同的作業系統字型點陣化引擎、相同的瀏覽器版本,每次都會繪出完全相同的像素——因此追蹤者在你第一次造訪和第五百次造訪時看到的是同一個值,完全不需要 Cookie。正是這種持久性,讓 Canvas 一開始就能作為一種長期識別碼發揮作用。
但這段雜湊並非永久不變,只要算繪鏈中任何一層發生變化,它就會跟著改變:
- GPU 驅動程式更新可能讓反鋸齒或曲線填滿的取整方式偏移幾個次像素,這就足以讓雜湊翻轉。
- 作業系統升級通常會附帶全新的字型點陣化引擎(新版 DirectWrite、Core Text 或 FreeType),悄悄改變每一個字形的繪製方式。
- 瀏覽器更新可能改變
toDataURL()編碼 PNG 資料的方式,或改變 Canvas 2D 繪圖環境(context)的反鋸齒處理,與作業系統無關。 - 更換硬體(新筆電、不同的 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 的追蹤防護文件描述了一套建立在智慧型追蹤防護(Intelligent Tracking Prevention)之上的更廣泛策略:按頂層網站分割儲存空間、限制指令碼存取跨站資料的能力,並把持久性的跨站識別行為整體視為應被封鎖的濫用模式,而不是逐一針對每個 API 修補。Canvas 讀取在 Safari 中依然能正常運作,但 ITP 真正瞄準的,是讓指紋對追蹤者有價值的那層跨站關聯能力。
Chrome 與原版 Chromium(沒有任何 Canvas 防護)
這一點值得直說,因為它涵蓋了大多數讀者:Chrome 沒有任何針對 Canvas 的防護。toDataURL() 和 getImageData() 交回來的,就是你的 GPU 與字型堆疊真實算繪出的像素,一般視窗與無痕視窗都一樣;那些沒有自行改造引擎的 Chromium 系瀏覽器也是同樣情形——上文的 Brave 是少數例外。Google 在這方面的隱私工程,瞄準的是跨站串接本身(第三方 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 這一側。


