一、問題背景
為了防止信息泄露或知識產權被侵犯,在web的世界里,對於頁面和圖片等增加水印處理是十分有必要的,水印的添加根據環境可以分為兩大類,前端瀏覽器環境添加和后端服務環境添加,根據實現方式又可以分為兩大類,顯性水印和數字水印。簡單對比一下這兩種方式的特點:
1、前端瀏覽器加水印:
(1)減輕服務端的壓力,快速反應
(2)安全系數較低,對於掌握一定前端知識的人來說可以通過各種騷操作跳過水印獲取到源文件
(3)適用場景:資源不跟某一個單獨的用戶綁定,而是一份資源,多個用戶查看,需要在每一個用戶查看的時候添加用戶特有的水印,多用於某些機密文檔或者展示機密信息的頁面,水印的目的在於文檔外流的時候可以追究到責任人
2、后端服務器加水印:
(1)當遇到大文件密集水印,或是復雜水印,占用服務器內存、運算量,請求時間過長
(2)安全性高,無法獲取到加水印前的源文件
(3)適用場景:資源為某個用戶獨有,一份原始資源只需要做一次處理,將其存儲之后就無需再次處理,水印的目的在於標示資源的歸屬人。
3、顯性水印:容易處理,算法較為簡單;可以通過裁剪、模糊等操作對水印進行攻擊消除,同時顯性水印也會破壞圖片的完整性。
4、數字水印:算法一般較為復雜,抗攻擊能力較強。
當然,優缺點也需要分情況來看,各個方案都擁有自己的優缺點,需要使用者在安全性、性能之間衡量。沒有最好的方案,只有根據環境與需求,使用當前最適合的方案。
二、調研 - 看看其他網站如何處理的
比如:CSDN、知乎、微博,都是直接 img 顯示 url,當獲取到 url
在瀏覽器下打開的時候,獲取到的是已經添加水印的圖片。(也有可能水印應該不止所見)
- 后端直接處理增加水印(前端直接使用
img
標簽顯示url
) - 暫時只能看到表層右下角水印(用戶名)
- 是否添加數字水印(未知)
從這三個網站的特點來看,這種做法是很合適的,因為他們需要添加水印的資源往往都綁定到了某一個用戶上,也就是,一份原始資源只需要做一次處理(前后端都可),將其存儲之后就無需再次處理。
但是,這種方式在某些情況下是卻不是最佳的。比如資源不跟某一個單獨的用戶綁定,而是一份資源,多個用戶查看,需要在每一個用戶查看的時候添加用戶特有的水印。這多用於某些機密文檔或者內部文件,水印的目的在於文檔外流的時候可以追究到責任人。故而,添加水印的方法需要根據環境的不同、需求的不同來變化。
三、實現方案
1、顯性水印 + DOM元素直接遮蓋 - 重復的 dom 元素覆蓋實現
從效果開始,要實現的效果是「在頁面上充滿透明度較低的重復的代表身份的信息」,第一時間想到的方案是在頁面上覆蓋一個 position:fixed 的div盒子,盒子透明度設置較低,設置 pointer-events: none;樣式實現點擊穿透,在這個盒子內通過 js 循環生成小的水印div,每個水印div內展示一個要顯示的水印內容。
頁面效果是有了,但是這種方案需要要在js內循環創建多個dom元素,既不優雅也影響性能,於是考慮可不可以不生成這么多個元素。
將水印文字直接通過一層DOM元素,覆蓋到需要添加水印的圖片上,並且可以添加兩層,一層為明顯水印,其透明度較高,肉眼可見,一層為隱藏水印,透明度極低,肉眼無法分辨,但可以通過一些處理后續顯現出來(以PS為例:現在把圖片放到PS里面,建一個圖層在上面,全部填充為黑色,混合模式選擇顏色加深這一類的(也就是讓亮的更亮,暗的更暗))
這樣,在用戶截圖的之后,就算塗抹掉了明顯水印,可由於隱藏水印肉眼無法分辨,簡單的塗抹攻擊並不能准確定位到隱藏水印。
當對於圖片完整性要求不高(也就是鋪滿了水印都不介意,只要看清內容即可)的情況,建議增加水印密度,直到只要用戶去塗抹水印,就會直接破壞文件到無法閱讀的地步
2、顯性水印+Canvas:canvas 輸出背景圖,或 svg 實現背景圖
第一步還是在頁面上覆蓋一個固定定位的盒子,然后創建一個canvas畫布,繪制出一個水印區域,將這個水印通過toDataURL方法輸出為一個圖片,將這個圖片設置為盒子的背景圖,通過backgroud-repeat:repeat;樣式實現填滿整個屏幕的效果。
與canvas生成背景圖的方法類似,只不過是生成背景圖的方法換成了通過svg生成,canvas的兼容性略好於svg。
其實現和顯性水印+DOM元素直接遮蓋一樣,但其性能優於方案一,直接通過Canvas繪畫,避免了在水印密度較大的情況下大量DOM元素的創建與添加,並且Canvas在部分環境與瀏覽器下擁用GPU加速的功能,故而性能提升較大。
具體一些實現代碼參考,可以看這篇文章:https://mp.weixin.qq.com/s/7NxQMtolD3UL5qDBsDkIWw
四、如何防止刪除 dom 元素去除水印
這樣看起來能滿足我們的需求了,但是還有一個問題,稍微懂一點瀏覽器的使用或網頁知識的用戶,可以用瀏覽器的開發者工具來動態更改DOM的屬性或者結構就可以去掉了。我們可以使用 MutationObserver 來監聽 dom 元素變化:MutationObserver給開發者們提供了一種能在某個范圍內的DOM樹發生變化時做出適當反應的能力。
MutationObserver兼容性可以看出高級瀏覽器以及移動瀏覽器支持非常不錯。突變觀察員 API 用來監視DOM變動。DOM的任何變動,比如節點的增減,屬性的變動,文本內容的變動,這個 API 都可以得到通知。
使用MutationObserver的實例的觀察函數方法用來啟動監聽,它接受兩個參數:第一個參數:所要觀察的DOM節點,第二個參數:一個配置對象,指定所要觀察的特定變動,有以下幾種:
屬性 | 描述 |
childList | 如果需要觀察目標節點的子節點(新增了某個子節點,或者移除了某個子節點),則設置為true. |
attributes | 如果需要觀察目標節點的屬性節點(新增或刪除了某個屬性,以及某個屬性的屬性值發生了變化),則設置為true. |
characterData | 如果目標節點為characterData節點(一種抽象接口,具體可以為文本節點,注釋節點,以及處理指令節點)時,也要觀察該節點的文本內容是否發生變化,則設置為true. |
subtree | 除了目標節點,如果還需要觀察目標節點的所有后代節點(觀察目標節點所包含的整棵DOM樹上的上述三種節點變化),則設置為true. |
attributeOldValue | 在attributes屬性已經設為true的前提下,如果需要將發生變化的屬性節點之前的屬性值記錄下來(記錄到下面MutationRecord對象的oldValue屬性中),則設置為true. |
characterDataOldValue | 在characterData屬性已經設為true的前提下,如果需要將發生變化的characterData節點之前的文本內容記錄下來(記錄到下面MutationRecord對象的oldValue屬性中),則設置為true. |
attributeFilter | 一個屬性名數組(不需要指定命名空間),只有該數組中包含的屬性名發生變化時才會被觀察到,其他名稱的屬性發生變化后會被忽略. |
MutationObserver只能監測到某種屬性改變,增減子結點等,對於自己本身被刪除,是沒有辦法的,可以通過監測父結點來達到要求。檢測代碼實現如下:
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; if (MutationObserver) { let mo = new MutationObserver(function () { const __wm = document.querySelector('.__wm'); // 只在__wm元素變動才重新調用 __canvasWM
if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) { // 避免一直觸發
mo.disconnect(); mo = null; __canvasWM(JSON.parse(JSON.stringify(args))); } }); mo.observe(container, { attributes: true, subtree: true, childList: true }) }
Mutation Observer API 用來監視 DOM 變動。DOM 的任何變動,比如節點的增減、屬性的變動、文本內容的變動,這個 API 都可以得到通知。
概念上,它很接近事件,可以理解為 DOM 發生變動就會觸發 Mutation Observer 事件。但是,它與事件有一個本質不同:事件是同步觸發,也就是說,DOM 的變動立刻會觸發相應的事件;Mutation Observer 則是異步觸發,DOM 的變動並不會馬上觸發,而是要等到當前所有 DOM 操作都結束才觸發。
這樣設計是為了應付 DOM 變動頻繁的特點。舉例來說,如果文檔中連續插入1000個<p>
元素,就會連續觸發1000個插入事件,執行每個事件的回調函數,這很可能造成瀏覽器的卡頓;而 Mutation Observer 完全不同,只在1000個段落都插入結束后才會觸發,而且只觸發一次。
Mutation Observer 有以下特點:
(1)它等待所有腳本任務完成后,才會運行(即異步觸發方式)。
(2)它把 DOM 變動記錄封裝成一個數組進行處理,而不是一條條個別處理 DOM 變動。
(3)它既可以觀察 DOM 的所有類型變動,也可以指定只觀察某一類變動。
關於 Mutation Observer API 的詳細介紹及具體如何使用,可以看這篇文章:《Mutation Observer API》- http://javascript.ruanyifeng.com/dom/mutationobserver.html