又是一個有關安全的問題。
通常狀況下,咱們說的水印是指圖片角落上的平臺用戶名水印。相似於下方圖片上的這種,一般只要將圖片上傳到平臺上,平臺就會在圖片上嵌入水印,固然,有些平臺也會提供設置是否須要顯示這種水印的開關,或者設置保存的時候纔會加上水印。css
這種水印的實現實際上是比較簡單的,就是將兩張圖片合成一張,或者是直接在原圖上繪製內容就好了:html
<img id="pic" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3c3c98ebfce4ae28db981dfabedc1d8~tplv-k3u1fbpfcp-zoom-1.image" alt="原始圖片" height="500" crossorigin="anonymous"> <div>Photo by Claudio Schwarz | @purzlbaum on Unsplash</div>
window.onload = () => { const pic = document.querySelector('#pic'); const canvasNode = document.createElement('canvas'); const picWithWatermark = createImageWithWatermark(pic, canvasNode); pic.src = picWithWatermark; } /** * 建立帶水印的圖片 * create image with watermark. * @param {HTMLImageElement} img 圖片結點 - image element. * @param {HTMLCanvasElement} canvas canvas結點 - canvas element. * @returns 處理後的圖片 base64 - pic with watermark. */ const createImageWithWatermark = (img, canvas) => { const imgWidth = img.width; const imgHeight = img.height; canvas.width = imgWidth; canvas.height = imgHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, imgWidth, imgHeight); ctx.font = '16px YaHei'; ctx.fillStyle = 'black'; ctx.fillText('Photo by Claudio Schwarz | @purzlbaum on Unsplash', 20, 20); return canvas.toDataURL('image/jpg'); }
以上就是完整的代碼了,更詳細的代碼能夠訪問github連接查看。前端
普通用戶所說的水印就是上面這種了,可是對於開發者來講,水印所包含的分類仍是比較多的。node
如咱們在公司內網的部分系統(也多是全部)上就能看到這種水印。git
這裏水印顏色選擇黑色只是爲了能更直觀的看到效果,真實使用這種水印的時候,都會選用白色透明的。
這種水印就有點相似以前所說的,將兩張圖片合成一個的那種方式,只不過,在前端頁面上,咱們是使用一個透明的canvas容器覆蓋整個頁面,而後在canvas中繪製這個「標識」,用來標識訪問當前頁面的用戶身份,這樣一來,不管是你截圖仍是拍照,只要圖片上能看到水印,咱們就能根據這個水印去追蹤到泄露這部分信息的人。github
那可能會有人問,那我知道這個水印是一個dom結點了,打開控制檯找到他,刪了不就行了?算法
這確實是好問題,不過也不是什麼大的問題,你想刪,這是徹底能夠的。canvas
我控制不了你的行爲,可是我能夠檢測到你操做了這個dom結點,那很差意思,我無論你怎麼操做的這個結點,爲了安全,我確定都要從新繪製這個水印的。後端
但光從新繪製水印我以爲還不夠,這可能會讓你跟我拼速度的,那不行啊,我必須給你點教訓的,還不能讓你得償所願,怎麼辦?只要你操做了個人dom,那麼我直接讓頁面白屏,而後再重載頁面。這也就達成了禁止用戶操做dom結點的方式了。安全
要實現這個,咱們須要藉助js提供的MutationObserver函數,這個函數能夠監聽容器的變化。
代碼以下:
// 容器監聽的回調 const cb = function (mutationList, observer) { for (const mutation of mutationList) { if (mutation.type === 'childList') { const { removedNodes = [] } = mutation; // 若是監聽到水印容器變化,那麼就清空頁面並重載 const node = Array.prototype.find.apply(removedNodes, [(node => node.id === 'page-watermark')]) if (node) { targetNode.innerHTML = ''; window.location.reload(); } } } } // 目標DOM結點 const targetNode = document.querySelector('#watermark-body'); // 建立監聽 const observer = new MutationObserver(cb); observer.observe(targetNode, { attributes: true, childList: true });
MutationObserver
是DOM3 Event規範的一部分,用於替代舊的Mutation Events,能夠放心使用。
雖然上面的是全局水印,可是你也能夠只對一部份內容加水印,只不過全局水印實現成本更低,代價小,對於內網系統來講,犧牲這點用戶體驗,並不能算是什麼很是嚴重的問題,是能夠接受的。
可能有人又要說了,我都打開dom,那我研究一下這個dom結構,寫個爬蟲去爬數據,或者直接複製dom裏面的內容不就行了,你這水印還有啥存在的意義嗎?
沒法反駁,可是要說明一點的是,爬數據這個是違法的,要負法律責任,並且你爬蟲確定是要運行在某個電腦上的,這就不須要水印了,咱們能夠直接查ip,追蹤到對應的人就好了,而咱們加的水印不過就是一個方便追蹤的工具而已。
其次,前端和爬蟲鬥智鬥勇,你從網頁爬數據,那我就想辦法不直接生成文字,而是把一些關鍵詞給替換成圖片,這樣一來,你爬蟲爬到的結果,就是一串沒有用的文字。
這就扯到反爬蟲的事情上了。言歸正傳,到目前爲止,咱們一直都在討論明水印,對於內網來講,使用這種水印確定是沒什麼問題的,可是對外的網站怎麼辦呢?若是也加上這種明水印,顯然不太合適,想要在這裏犧牲用戶體驗就是不能接受的。
因此咱們就開始考慮,能不能加上一個肉眼看不見的水印呢?
固然是沒問題的,這就是咱們下面要說的暗水印。
聽名字就知道,暗水印和明水印是恰好相反的,咱們看不見這種水印,並且這種水印不管是原理仍是實現,和明水印的差異都是比較大的。
先看看原理。
不知道你有沒有據說過,隱寫術1。對於這個比較玄幻的名詞,wiki是這麼描述的「隱寫術是一門關於信息隱藏的技巧與科學,所謂信息隱藏指的是不讓除預期的接收者以外的任何人知曉信息的傳遞事件或者信息的內容。」,究其本質,仍是密碼學那一套。
咱們能夠經過各類方式將信息寫到圖片,最多見的應該是將須要隱寫的內容以二進制的形式寫入圖片中,我們在這裏舉個簡單的例子,如下面的圖片爲例:
這是咱們開篇引用的圖片,記爲原始圖像,將圖片保存在本地後(original.png),執行命令:
tail -c 50 1.png
能夠看到執行結果裏面是一串亂碼(用Hex查看器能夠看到文件的二進制碼流,這裏是utf-8,亂碼是正常的),對該文件執行命令:
cat original.png > result.png echo testWrite >> result.png tail -c 50 result.png
咱們生成一張新的圖片以後,將一串字符追加到圖片末尾,能夠看到圖片依舊是正常顯示的,同時查看圖片的內容,能夠看到剛纔寫入的testWrite字符串:
另外,將字符串加到文件頭部是不行的,由於文件頭部包含了文件格式等信息。若是你把信息插入到文件頭部,市面上的軟件就沒法正確的識別文件的類型。
固然了,你能夠本身設計編碼解碼器來建立新的文件類型。
這只是一種方式,並且手段十分暴力,處理以後的圖片文件較原來的文件是有必定的大小變化的(不過比較小,能夠按字節計算)。更聰明的作法是將加密的信息按照某種模式寫入圖片的二進制流中,這樣一來,就只有加密方纔能拿到對應的信息了。
但即便有複雜的加密方式,也仍是不夠的,由於這隻能保證別人在使用原始圖片的時候,咱們能夠鑑別圖片的來源、流傳路線,但要是經過屏幕截圖或者拍照的方式,咱們就沒法拿到這個數據,由於此時相對於咱們作過處理的圖片,他已是一張全新的圖片了。
來看另外一個例子,RGB份量值的小量變更:在圖片上覆蓋一層肉眼看不見的圖片,簡單來講就是我能夠在圖片的某個單通道(如rgb中的b通道)內將水印信息寫入,其實這麼說也仍是很難懂,舉個例子:
如今要將左右兩側的圖片組合,可是不能讓右側的圖片內容在左側的圖片上觀察到,這時候咱們要作的就是按照必定規則將水印圖片寫進這張圖片的rgb通道內。
預處理,先生成右側的水印圖 編碼 1. 經過canvas獲取到兩張圖片的rgba數據 2. 將左側圖片的b(藍色)通道值-1,即,b & 0xfffffffe 3. 讀取右側b通道數據,遇到大於0的值,就將左側對應位置處的b通道值 +1,即,b | 0x00000001 解碼 1. 獲取圖片的rgba數據 2. 讀取b通道數據,遇到 b & 0x00000001 > 0 的數據,說明有水印信息,將其置爲255,除a通道(alpha通道不是顏色通道)外,其他通道的數據所有置爲0 // +1,-1 是由於量級的變化極小,並不會影響到圖片的顯示
其實黑底藍字的圖片就是解碼出來的水印數據,詳細代碼:
好像這種方式能夠在用戶截圖時也可以保留咱們的水印?其實並無。
這是解碼截圖的結果,能夠明顯的看到,QQ截圖以後的圖片並無可以解碼出來咱們所須要的水印內容,甚至於將圖片壓縮以後,可能就會失去咱們的水印,因此說這其實也並非一個可靠的水印方式。
那如何才能保證咱們的水印至少在截圖的時候也能發揮做用呢?
也不是不行,首先肯定咱們水印要加在哪裏(肯定需求),由於圖片來源無非是網頁搜索結果,或者說咱們截得圖多數來自於網頁,因此咱們考慮的是在網頁上覆蓋一層水印,保證用戶從網頁上截取的圖片能夠被咱們追蹤到來源。
這個通用的解決方案依舊是寫css,只不過這時候咱們將背景圖置頂,同時將其透明度設置的很低。
代碼很簡單,其實就是將一張背景圖片鋪滿整屏就能夠了,而後將opacity設置到肉眼沒法觀察到的程度就OK了:
window.onload = () => { const width = document.body.clientWidth; const height = document.body.clientHeight; const maskDiv = document.createElement('div'); maskDiv.id = 'mask_watermark'; maskDiv.style.position = 'absolute'; maskDiv.style.backgroundImage = 'url(./1.jpg)'; maskDiv.style.backgroundRepeat = 'repeat'; maskDiv.style.visibility = ''; maskDiv.style.left = '0px'; maskDiv.style.top = '0px'; maskDiv.style.overflow = "hidden"; maskDiv.style.zIndex = "9999"; maskDiv.style.pointerEvents = "none"; maskDiv.style.opacity = 0.005; maskDiv.style.fontSize = '20px'; maskDiv.style.color = '#000'; maskDiv.style.textAlign = "center"; maskDiv.style.width = `${width}px`; maskDiv.style.height = `${height}px`; maskDiv.style.display = "block"; document.body.appendChild(maskDiv); }
左側是從網頁上接下來的圖片,右側是在PS工具中處理以後的圖片2,明顯能夠看到咱們設置的水印。
而生成圖片的方式就有不少種了,能夠是前端生成,也能夠是將信息發給後端,後端生成一張圖片,而後前端將圖片做爲背景圖。
想要獲得右側的結果,未必須要PS進行處理,能夠經過其餘的方式進行處理。
到這裏,前端部分就結束了,但可能有人還以爲這不太行,我截網頁的圖如今是加上了水印,可是我要是保存原圖呢?那能夠用以前說的RGB份量那個方式。
那我下載圖片以後在原圖上截取呢,不就失效了?確實,到這裏前端能作的工做已經不多了。咱們已經處理不到了,可是在圖像暗水印,或者說盲水印這個領域,還有更加有效的抵抗攻擊(去水印)的方式,好比頻域、空域的變換。這個變換能夠說是老生常談的了,我就不過多解釋了。
水印的概念是泛化的,並非說只有顯示在圖片某個角落的信息才能被稱爲水印。
上面選擇將信息追加到文件末尾是有緣由的,不是瞎選的。任何一種文件都包含文件結束符,就如文件頭部約定存放文件的格式信息同樣,即便你改了後綴,我也能經過讀取這個文件頭部的內容來識別文件真實的格式。
另外咱們知道,文件後綴名是能夠隨意更改的,若是隻經過文件後綴名進行檢測,那麼絕對是能夠繞過的,進而出現任意文件上傳的安全問題。
若是改變圖層混合模式沒能成功,不妨試下修改圖像的RGB曲線