​探祕 Web 水印技術

語言: CN / TW / HK

作者:fransli,騰訊 PCG 前端開發工程師

Web 水印技術在信息安全和版權保護等領域有着廣泛的應用,對防止信息泄露或知識產品被侵犯有重要意義。水印根據可見性可分為可見水印和不可見水印(盲水印),本文將分別予以介紹,帶你探祕 web 水印技術。

可見水印

最簡單的水印

一種比較常見的簡單水印場景是給文章、表格加上 logo 水印,用以申明版權。

這裏想要的效果就是一個淺淺的 logo 平鋪展示。實現起來也比較簡單,只需製作一個半透明的 logo 圖片,設為文章或者表格的背景圖片即可。僅需一行 CSS 聲明。

background-image:url("./logo.png");

實現圖片平鋪關鍵的 CSS 屬性是 background-repeat ,值為 repeat 時是平鋪,這也是它的默認值,所以可以省略。

全頁面水印

照葫蘆畫瓢,如果要給整個 Web 頁面加上水印,是不是給頁面的 body 元素設置背景圖片平鋪展示就可以了呢?

然而通常並不會這麼處理,因為文章和表格內容多以文本為主,不會明顯遮擋水印,而一個完整的頁面往往還包含很多其他頁面元素,比如圖片、視頻、控件等等,它們很可能會遮擋住背景圖片,從而影響水印效果。

所以,為了避免被其他元素遮擋,針對頁面的水印一般會使用一個層級比較高且覆蓋整個頁面的元素來承載。

div.watermark{
position: fixed;
left:0;
top:0;
width: 100vw;
height: 100vh;
background-image:url("./logo.png");
opacity: .5;
z-index: 3000;
}

這樣一來,其他元素就遮擋不住水印了。不過,這個 div 反過來可能會遮擋頁面其他元素,影響頁面元素操作。還需要一條關鍵的 CSS 聲明來破解這個問題 :

pointer-events: none;

這個 CSS 聲明會使該元素“可穿透”,“看得見、摸不着”,不再影響頁面操作。

動態水印

很多時候,給頁面加水印的目的並不是申明版權,而是為了支持溯源。此時水印的內容並不會只是一個 logo ,通常會包含用户信息,比如用户名、UID、手機號等等。

這就意味着,每個用户的水印內容是不同的,無法通過提前準備好一張圖片來滿足了。這種場景往往需要根據用户信息動態生成圖片。

我們來看下幾種主流的動態生成水印圖片的方式:

服務端方案

傳統的方式是在服務端生成圖片。頁面上發起的圖片請求中可以附帶用户信息,服務端根據這些參數動態生成圖片,並將圖片數據作為該請求的響應返給頁面,頁面拿到後將其用作水印。

這種方式的優點是兼容性好,缺點是需要前後端配合,增加了頁面請求和服務端資源開銷,防攻擊能力也較差。

Canvas 方案

HTML5 引入 Canvas 特性使得瀏覽器自身具備了繪圖能力。經過多年的發展,主流瀏覽器基本都已可以提供良好的支持。通過 Canvas 可以輕鬆繪製圖片,並可將圖片數據導出,用於頁面圖片或背景。

const canvasElement = document.createElement('canvas');
const context = canvasElement.getContext('2d');
canvasElement.width = 200;
canvasElement.height = 200;
context.rotate((-30 * Math.PI) / 180);
context.font = '400 26px Arial';
context.fillStyle = '#B9C0CA';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('水印文字', 70, 130);
const watermark = canvasElement.toDataURL('image/png');

通過上述示例代碼可拿到水印圖片的 data URI 數據,用作水印承載元素的背景圖片平鋪展示即可。

這種方式不需要服務端配合,在前端就可以完成,且有助於減少請求和服務端資源開銷。曾經面臨的瀏覽器兼容問題現在也不再是問題,該方案已逐漸流行起來。

SVG 方案

對於純文字的水印來説,有沒有辦法不生成圖片而直接實現平鋪呢?

動態創建大量 DOM 節點,通過 CSS 控制排列當然可以實現,但是繁瑣且性能差,優雅更無從談起。

不妨換個角度思考,有沒有辦法讓文字不轉成圖片就可以用作 background-image 屬性的值呢?這樣就可以利用 background-repeat 實現平鋪效果了。

這時候可以考慮使用 SVG ,因為 SVG 具有文本和圖像的雙重特性。看上去是文本,然而在很多場景可以當做圖片使用。

我們可以通過 SVG 的相關屬性精準控制字體位置、大小、顏色、透明度和旋轉角度等參數。如:

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<text x="50%" y="50%" font-size="30" fill="#a2a9b6" fill-opacity="0.3" font-family="system-ui, sans-serif" text-anchor="middle" dominant-baseline="middle" transform='rotate(-45, 100 100)'>水印文字</text>
</svg>

考慮到瀏覽器兼容性,用作背景圖片時,建議將 SVG 編碼為 Base64 (或轉義特定字符):

background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1zaXplPSIzMCIgZmlsbD0iI2EyYTliNiIgZmlsbC1vcGFjaXR5PSIwLjMiIGZvbnQtZmFtaWx5PSJzeXN0ZW0tdWksIHNhbnMtc2VyaWYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRyYW5zZm9ybT0ncm90YXRlKC00NSwgMTAwIDEwMCknPuawtOWNsOaWh+WtlzwvdGV4dD48L3N2Zz4=");

水印安全

水印是用來保護信息安全的。信息要安全,首先要確保水印自身的安全,提高水印的防攻擊(篡改、刪除等)能力。

可見水印大都是基於 DOM 的,找到這個 DOM 節點,通過瀏覽器插件、抓包工具等在頁面上注入一段 JavaScript 或者 CSS 代碼對其進行篡改或刪除並不困難。

防止外部代碼篡改,一種思路是把水印元素封裝起來,與外部環境進行隔離。

Shadow DOM

Chrome 逐步統治瀏覽器江湖之後,谷歌正野心勃勃的推廣 Web Components 技術。該技術允許在 Web 中創建可重用的小部件或組件。組件化開發在前端業界已經流行相當長一段時間了,這主要得益於前端三大框架的推崇,但具體組件標準是由框架各自制定的,而 Web Components 可理解為 Web 標準化的組件。

Web Components 的一個重要特性就是 “封裝” ,即可以將標記結構、樣式和行為隱藏起來,並與頁面上的其他代碼相隔離。比如我們熟悉的 video 元素,它的進度條、按鈕等控件都已被封裝。

Shadow DOM 接口是 “封裝” 特性的關鍵所在,它可以將一個隱藏的、獨立的 DOM 附加到一個元素上。

為了提高 web 水印的隱蔽性,同時避免受外部代碼影響,從而在一定程度上防止篡改,可以考慮把水印元素放在 Shadow DOM 中。

來看下 Shadow DOM 的基本用法。使用 Element.attachShadow() 方法來將一個 shadow root 附加到任何一個元素上。它接受一個配置對象作為參數,該對象有一個 mode 屬性,值可以是 open 或者 closedopen 表示可以通過頁面內的 JavaScript 方法來獲取 Shadow DOM 。而 closed 則表示不可以從外部獲取 Shadow DOM

Element.attachShadow({mode: 'closed'});

樣式怎麼隔離呢? Shadow DOM 中的樣式本身就是隔離的,除非主動使用 CSS 變量、 part 屬性等暴露,外部樣式是不會影響到組件內的。

Mutation Observer

Shadow DOM 提高了水印的隱蔽性,同時可以防止外部代碼修改。除此之外,還有一種常見的攻擊場景——人為修改,比如在瀏覽器控制枱直接修改或刪除對應的 DOM 元素。

可以考慮 “監聽” 這種行為,一旦發生就馬上修復,比如重新插入一個。那怎麼實現這種 “監聽” 呢?現代瀏覽器中有多種觀察者( Observer ),比如 IntersectionObserverPerformanceObserverResizeObserverReportingObserverMutationObserver 等。其中, MutationObserver 就可以用來監聽 DOM 變動, DOM 的任何變動,比如節點的增減、屬性的變動、文本內容的變動,通過該 API 都可以得到通知。

所以可以使用 MutationObserver API 來監聽水印元素 DOM 變化,一旦監聽到 DOM 元素被修改或者刪除,就立即重新插入一個。

不可見水印(盲水印)

不可見水印也叫盲水印、隱水印,顧名思義是一種看不到的水印,看不到還要它做什麼呢?其實,不可見水印在一些對安全性要求較高的場景意義還是蠻大的。不可見水印通常具有比可見水印更好的隱蔽性和抗攻擊性。雖不可見,但通過一定的技術手段是可以將水印信息從其載體上提取出來的,這就使得其載體具備了溯源能力,在關鍵時刻往往能發揮大作用。

我總結不可見水印相對可見水印至少有以下三個明顯的優勢:

  • 更好的觀感。可見水印總給人一種“膏藥感”,甚至會引起部分人的不適,而不可見水印則不會有這個問題。

  • 更佳的隱蔽性。用户基本感知不到水印的存在。

  • 更強的抗攻擊性。可見水印更容易受到攻擊,而不可見水印除了隱蔽性比較強之外,其自身往往還具備比較強的抗攻擊能力。

不可見水印(盲水印)屬於信息隱匿技術(也叫隱寫術),歷史悠久,手段繁多。在現代,隨着計算機網絡技術的發展,數字產品的信息安全和版權保護也已成為信息隱匿技術的一個重要課題。隱寫術在數字音頻、數字視頻和數字圖像領域有着非常廣泛的應用。

Web 上基於 DOM 的盲水印大都不靠譜,而另一方面數字圖像是信息隱藏和數字水印領域研究最多和最早的一種載體, 相較於 Web,數字圖像領域有着更為成熟的數字水印算法 。我們不妨先來看下數字圖像領域的常見盲水印技術。

在説數字水印之前,這裏介紹一些數字圖像的基礎知識。

數字圖像(位圖)是由像素( pixel )組成。

  • 1
    bit
    1
    1
    byte
    8
    
  • 1
    256
    2
    8
    8
    bit
    1
    
  • RGB
    R
    G
    B
    256
    1
    3
    1
    
  • RGBA
    R
    G
    B
    256
    1
    4
    

通常,考慮到計算速度和性能,圖像處理和圖像識別大都會將圖像轉成灰度圖或者選擇其中一個通道進行。

LSB 水印

如上文所述,灰度圖像的一個像素有 256 種狀態,通常用灰度值( 0-255 )表示, 0 表示黑色, 255 表示白色,灰度值越大表示亮度越高。

灰度可用一個字節,即 8 比特二進制數表示,其中最高位對圖像的貢獻最大,最低位對圖像的貢獻最小,稱為最低比特位( Least Significant BitLSB )。

如果將一個圖像所有像素的比特位抽出來,就構成了 8 個不同的位平面,從 LSB (最低有效位 0 )到 MSB (最高有效位 7 )。位平面從低位到高位,圖像的特徵逐漸變得複雜,細節不斷增加,相鄰比特的相關性也越強。而比特位越低包含的圖像信息就越少,最低位平面類似於隨機噪聲。因此,改變低位對圖像的成像質量影響不大。

LSB 水印就是利用了這一點,用水印信息替換載體圖像的最低比特位,這樣原圖像的 7 個高位平面就與表示水印信息的最低位平面組成了新的圖像。

LSB 水印魯棒性(防攻擊性)較差,水印信息容易被抹去。

頻域水印

將數字圖像用一個矩陣來表示,是圖像的空間域表示方法, LSB 就是在圖像的空間域隱藏信息,魯棒性較差。而在圖像信號的頻域(變換域)中隱藏信息要比在空間域中隱藏信息具有更好的魯棒性。那麼如何把圖像信號從空間域轉換到頻域呢?這裏就需要用到大名鼎鼎的 傅里葉變換 了。

法國數學家傅里葉大家一定不陌生,高數裏就有傅里葉級數。

傅里葉提出的傅里葉變換( Fourier transform )理論,表示能將滿足一定條件的某個函數表示成三角函數(正弦和/或餘弦函數)或者它們的積分的線性組合,可用於把信號從時間域(或空間域)變換到頻率域。

在此之前人們對信號的分析主要集中在空間域,傅里葉變換的提出為頻域分析奠定了基礎,有助於解決許多圖像的問題。

傅里葉變換在數字圖像處理領域有着極為重要的應用,圖像領域變換的實質是把圖像函數展開成具有不同空間頻率的正、餘弦信號的疊加,也就是説任何圖像都可以分解為若干個頻率不同的亮度呈正弦變化的圖像之和。把圖像從空間域變換到頻率域後,就能夠實現對圖像數據進行不同頻率成分的提取。對於圖像信號來説,可以把灰度(亮度)看做頻率,傅里葉變換可作為圖像灰度值形成的空間域與其頻率域的橋樑。

在頻域中隱藏信息就是傅里葉變換在數字圖像處理領域的一個典型應用場景。通常多選擇在圖像頻域的中頻部分嵌入信息,因為高頻部分易於被各種信號處理方法破壞,而人的視覺又對低頻部分比較敏感,容易察覺低頻部分的變化。

在圖像頻域嵌入水印信息的大致流程為:把原始圖像通過離散傅里葉變換轉換到頻域(變換域),把水印文字信息混入,再經過離散傅里葉變換的逆變換,便得到了載有水印信息的圖像。水印信息隱匿性較好。

光説不練假把式,操練起來。

下圖是我隨手拍的鵝廠北京總部大樓一角。

對上圖的一個通道進行離散傅里葉變換,在其變換域(頻域)加入水印文字(fransli)後,再進行離散傅里葉變換的逆變換,便得到了下圖。怎麼樣,看不到水印信息吧?

對上圖進行離散傅里葉變換,將圖片轉換到頻域(變換域),我們可以清楚的看到嵌入的水印文字(下圖)。

頻域盲水印具有比較好的防攻擊性,我們來測試一下。

我們截取圖像中的一部分並重新採樣,然後嘗試提取水印信息。

可以看到還是有很大概率可以提取到有效水印信息的。

Web 中的數字水印應用

上面介紹了幾種常見的不可見水印(盲水印)實現方式,其中魯棒性比較好的是基於頻域的數字圖像盲水印,但這種水印主要是針對數字圖像,而 Web 上的內容載體並不一定都是圖片,常見的需要加水印的載體除了圖片還有文本、表格等,這些場景該如何應用頻域盲水印呢?

或許, Canvas 就是答案。

Reference