前端水印
為什麼要有水印的存在?
- 保護智慧財產權,防止未經允許被隨意盜用。比如淘寶美團的圖片,背後都有水印。
- 保護公司機密資訊,防止有心之人洩密
通常來說,前後端都可以實現水印的新增。
- 前端水印適用場景:資源不跟某一個單獨的使用者繫結,而是一份資源,多個使用者檢視,需要在每一個使用者檢視的時候新增使用者特有的水印,多用於某些機密文件或者展示機密資訊的頁面,水印的目的在於文件外流的時候可以追究到責任人
- 服務端水印使用場景:資源為某個使用者獨有,一份原始資源只需要做一次處理,將其儲存之後就無需再次處理,水印的目的在於標示資源的歸屬人
從前端的角度來說,有哪些實現方案
DOM覆蓋
利用div來做水印,需要兩個關鍵css屬性。use-select:none
和pointer-events
,不讓使用者選中我這個水印,以及讓使用者穿透我這個水印遮罩。
然後利用想要出現水印區域的寬高以及水印塊的寬高,計算出我需要生成多少個水印塊,然後鋪開。
initDivWaterMark(userId: string) {
const waterHeight = 100
const waterWidth = 100
const { clientWidth, clientHeight } =
document.documentElement || document.body
const column = Math.ceil(clientWidth / waterWidth)
const rows = Math.ceil(clientHeight / waterHeight)
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div')
wrap.setAttribute('class', 'watermark-item')
wrap.style.width = waterWidth + 'px'
wrap.style.height = waterHeight + 'px'
wrap.textContent = userId
this.box.appendChild(wrap)
}
}
ok,可以看到,我們的水印出現了。但是有一個問題是,使用dom重複生成的話,還不停的append
的話,感覺不優雅。而且一下就被人看到了,所以也可以用shadowdom
。
shadowdom ShadowDom MDN
Web components 的一個重要屬性是封裝——可以將標記結構、樣式和行為隱藏起來,並與頁面上的其他程式碼相隔離,保證不同的部分不會混在一起,可使程式碼更加乾淨、整潔。其中,Shadow DOM 介面是關鍵所在,它可以將一個隱藏的、獨立的 DOM 附加到一個元素上。說白了就是隔離。
可以使用 Element.attachShadow()
方法來將一個 shadow root
附加到任何一個元素上。它接受一個配置物件作為引數,該物件有一個 mode
屬性,值可以是 open
或者 closed
:
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open 表示可以通過頁面內的 JavaScript 方法來獲取 Shadow DOM,例如使用 Element.shadowRoot 屬性:
let myShadowDom = myCustomElem.shadowRoot;
如果你將一個 Shadow root
附加到一個 Custom element
上,並且將 mode
設定為 closed
,那麼就不可以從外部獲取 Shadow DOM
了, myCustomElem.shadowRoot
將會返回 null。瀏覽器中的某些內建元素就是如此,例如<video>
,包含了不可訪問的 Shadow DOM
。
所以為了簡單,我們用了closed
initShadowdomWaterMark(userId: string) {
const shadowRoot = this.box.attachShadow({ mode: 'closed' })
const waterHeight = 100
const waterWidth = 100
const { clientWidth, clientHeight } =
document.documentElement || document.body
const column = Math.ceil(clientWidth / waterWidth)
const rows = Math.ceil(clientHeight / waterHeight)
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div')
wrap.setAttribute('class', 'watermark-item')
// const styleStr = `
// color: #f20;
// text-align: center;
// transform: rotate(-30deg);
// `
// wrap.setAttribute('style', styleStr)
// wrap.setAttribute('part', 'watermark')
wrap.style.width = waterWidth + 'px'
wrap.style.height = waterHeight + 'px'
wrap.textContent = userId
shadowRoot.appendChild(wrap)
}
}
可有看到,這樣寫,樣式沒有了,其實就是因為 shadowdom
起到了隔離的作用,微前端裡很重要的一個點就是沙箱隔離,其中像qiankun
這樣的框架的css
隔離就是用了shadowdom
使用內聯或者使用特殊的:偽類也可以解決,這裡就先直接內聯了。 Css part
canvas/svg背景圖
可以看到,不管是使用dom還是shadowdom,都避免不了的進行for迴圈來進行新增元素,還需要計算。依然不是那麼的優雅。所以我們可以考慮用canvas輸出一個背景圖,然後通過background-repeat: repeat
來實現。
getCanvasUrl(userId: string) {
const angle = -30
const txt = userId
this.canvas = document.createElement('canvas')
this.canvas.width = 100
this.canvas.height = 100
this.ctx = this.canvas.getContext('2d')!
this.ctx.clearRect(0, 0, 100, 100)
this.ctx.fillStyle = '#f20'
this.ctx.font = `14px`
this.ctx.rotate((Math.PI / 180) * angle)
this.ctx.fillText(txt, 0, 50)
return this.canvas.toDataURL()
}
可以看到,我們只用了一個標籤,以及背景圖的方式,來實現了一個水印,但是呢,如果你是一個有心之人,我們只需要動動手指,開啟F12,把這個標籤刪了,或者修改它的背景,都可以把水印去掉。那我們應該怎麼辦呢?那就需要用到MutationObserver
了,說到觀察者,現在使用的頻率也是越來越高了。
MutationObserverMDN
MutationObserver應用的方式還挺多的
- 如我們上週提到的
guide mask
的解決方案問題。我們可以通過mutationObserver
對它要籠罩的父級節點進行監控,並設定一個超時時間disconnect
,在時間內對它進行矯正。我曾經有做過一個錨點的功能,也利用到了它來進行矯正操作。 - 我們可以通過
MutationObserver
來對真正的可用效能進行監控,通過判斷節點的增加趨勢,來獲得真正可以使用的時間點。 Vue nexttick
的實現原理,利用MutationObserver
是個micro task
,來進行下一tick的通知。當然是promise.then
不好使的情況下,模擬實現如下function myNextTick(func) { var textNode = document.createTextNode('0'); //新建文字節點 var callback = (mutationsList, observer) => { func.call(this); }; var observer = new MutationObserver(callback); observer.observe(textNode, { characterData: true }); textNode.data = '1'; //修改文字資訊,觸發dom更新 }
- 監控我們的水印節點是否被變更,我們也可以針對性的進行恢復。
initObserver() {
// 觀察器的配置
const config = { attributes: true, childList: true, subtree: true }
// 當觀察到變動時執行的回撥函式
const callback: MutationCallback = (mutationsList, observer) => {
for (const mutation of mutationsList) {
mutation.removedNodes.forEach((item) => {
if (item === this.box) {
this.warnningTargetChanged = true
// 省事,直接新增在body上了
document.body.appendChild(this.box)
}
})
}
if (this.warnningTargetChanged) {
this.warnningTargetChanged = false
console.log(`使用者${this.userId}的水印變動了,可能涉嫌違規操作!!!`)
}
}
// 監聽元素
const targetNode = document.body
// 建立一個觀察器例項並傳入回撥函式
const observer = new MutationObserver(callback)
// 以上述配置開始觀察目標節點
observer.observe(targetNode, config)
}
當然,並不是說這樣就萬無一失了,因為我們還可以通過 disabled javascript來解決。像有的網站,開啟F12就會無限迴圈debugger,也可以解決。
暗水印
那如果說,就是有這樣的人存在,統統都能搞定呢?這時候就需要隱藏水印的出現了。比如大眾點評上面的圖片,其實也都是有隱藏版權的水印存在的。如果是商用盜用,都是能被查到的。
暗水印的生成方式有很多,常見的為通過修改RGB 分量值的小量變動
、DWT、DCT 和 FFT 等方法。DFT、DCT和DWT的聯絡和區別。
前端實現主要看RGB 分量值的小量變動。
我們都知道圖片都是有一個個畫素點構成的,每個畫素點都是由 RGB 三種元素構成。當我們把其中的一個分量修改,人的肉眼是很難看出其中的變化,甚至是畫素眼的設計師也很難分辨出。
那麼,我們只要能獲取到一張圖片上每個畫素點上的具體資訊,就可以再RGB上動動手腳,就可以把我們想要的資訊藏進去。那如何獲取畫素點的資訊呢?
就需要用到 canvas 的 CanvasRenderingContext2D.getImageData()
了,這個方法會返回一個 ImageData
物件,其中就包含了畫素的資訊陣列。所以我們應該可以利用這個方法,來做取色器
這個一維陣列儲存了所有的畫素資訊,一共有 256 256 4 = 262144 個值。其中 4 個值一組,為什麼呢?在瀏覽器中解析圖片,除了 RGB 值外,每組第 4 個值為透明度值,即畫素資訊實際為大家熟知的 rgba 值。
以我們想要藏的文字資訊為例
const txt = '測試點'
this.canvas = document.createElement('canvas')
this.canvas.width = 10
this.canvas.height = 10
this.ctx = this.canvas.getContext('2d')
this.ctx.clearRect(0, 0, 10, 10)
this.ctx.font = `14px`
this.ctx.fillText(txt, 0, 0)
const textData = this.ctx.getImageData(0, 0, 10, 10).data
把上面程式碼複製到控制檯可以發現,字型的資料基本都是0,0,0,xx。
那現在我們有了文字資料和圖片資料,我們就可以設計一個演算法。以R通道為例子,這個是紅色通道,(255, 0, 0, 255)
就代表的是純紅色。
我們遍歷圖片資料,檢查它的每一個R點位,如果這個點位的文字資料是有的,也就是alpha
值不為0,那我們就強行把當前圖片資訊的這個點的值改成奇數,如果這個點位沒有數字,就把它改成偶數。
那麼最後,這個圖片資料裡奇數的部分,就是有文字蓋著的部分。而偶數部分,就是無關緊要的了。那最後想要找到我們的目標文案的時候,只需要把奇數部分的值變成255,把其他通道以及偶數部分的,全都改成0。 文字就出現了。
// 加密核心方法
for (let i = 0; i < oData.length; i++) {
if (i % 4 === bit) {
// 如果文字在這裡沒有資料,並且圖片R通道是奇數,那我們把它改成偶數。
if (newData[i + offset] === 0 && oData[i] % 2 === 1) {
// 沒有資訊的畫素,該通道最低位置0,但不要越界
if (oData[i] === 255) {
oData[i]--
} else {
oData[i]++
}
// 如果文字在這裡是有資料的,並且圖片R通道是偶數,那我們把它改成奇數。
} else if (newData[i + offset] !== 0 && oData[i] % 2 === 0) {
oData[i]++
}
// 也就是說,如果是奇數,說明一定是有文字壓在上面的
}
}
// 解密核心方法
for (let i = 0; i < data.length; i++) {
// R通道
if (i % 4 === 0) {
// 目標分量,把偶數的關閉。因為文字沒有資料在這裡。
if (data[i] % 2 === 0) {
data[i] = 0
} else {
data[i] = 255
data[i + 3] = 255
}
} else if (i % 4 === 3) {
continue
} else {
data[i] = 0
}
}
但是我們實際想要見到的隱藏水印的形式,肯定不是侷限在圖片上的。我們希望的是給我們整體的文章之類的打上隱藏水印。那這怎麼做呢?
其實我們可以把之前cavans
的水印,把顏色換成黑色,旋轉去掉,透明度降低到0.005
,為啥是這個值,因為0.005 * 255=1.27
。基本算是最小透明度了。但是因為我們是高層級,所以截圖的時候,一定會把這資訊包含進去。那麼我是(0,0,0,1)
疊加到原來的圖片上,至少會影響到原圖的顏色,也就是說它的R通道,起碼會動個1。我編不下去了。我也不知道為啥。但是確實可以。大家可以一起探討下。