前端水印實現方案

1、問題背景

爲了防止信息泄露或知識產權被侵犯,在web的世界裏,對於頁面和圖片等增長水印處理是十分有必要的,水印的添加根據環境能夠分爲兩大類,前端瀏覽器環境添加和後端服務環境添加,簡單對比一下這兩種方式的特色:javascript

前端瀏覽器加水印:css

  • 減輕服務端的壓力,快速反應
  • 安全係數較低,對於掌握必定前端知識的人來講能夠經過各類騷操做跳過水印獲取到源文件
  • 適用場景:資源不跟某一個單獨的用戶綁定,而是一份資源,多個用戶查看,須要在每個用戶查看的時候添加用戶特有的水印,多用於某些機密文檔或者展現機密信息的頁面,水印的目的在於文檔外流的時候能夠追究到責任人

後端服務器加水印:html

  • 當遇到大文件密集水印,或是複雜水印,佔用服務器內存、運算量,請求時間過長
  • 安全性高,沒法獲取到加水印前的源文件
  • 適用場景:資源爲某個用戶獨有,一份原始資源只須要作一次處理,將其存儲以後就無需再次處理,水印的目的在於標示資源的歸屬人

這裏咱們討論前端瀏覽器環境添加前端

2、收益分析

簡單介紹一下目前主流的前端加水印的方法,之後其餘同窗在用到的時候能夠做爲參考。java

3、實現方案

1. 重複的dom元素覆蓋實現

從效果開始,要實現的效果是「在頁面上充滿透明度較低的重複的表明身份的信息」,第一時間想到的方案是在頁面上覆蓋一個position:fixed的div盒子,盒子透明度設置較低,設置pointer-events: none;樣式實現點擊穿透,在這個盒子內經過js循環生成小的水印div,每一個水印div內展現一個要顯示的水印內容,簡單實現了一下web

<!DOCTYPE html> 
<html> <head> <meta charset="utf-8"> <title></title> <style> #watermark-box { position: fixed; top: 0; bottom: 0; left: 0; right: 0; font-size: 24px; font-weight: 700; display: flex; flex-wrap: wrap; overflow: hidden; user-select: none; pointer-events: none; opacity: 0.1; z-index: 999; } .watermark { text-align: center; } </style> </head> <body> <div> <h2> 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- </h2> <br /> <h2> 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- </h2> <br /> <h2 onclick="alert(1)"> 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- 機密內容- </h2> <br /> </div> <div id="watermark-box"> </div> <script> function doWaterMark(width, height, content) { let box = document.getElementById("watermark-box"); let boxWidth = box.clientWidth, boxHeight = box.clientHeight; for (let i = 0; i < Math.floor(boxHeight / height); i++) { for (let j = 0; j < Math.floor(boxWidth / width); j++) { let next = document.createElement("div") next.setAttribute("class", "watermark") next.style.width = width + 'px' next.style.height = height + 'px' next.innerText = content box.appendChild(next) } } } window.onload = doWaterMark(300, 100, '水印123') </script> </body> </html> 複製代碼

頁面效果是有了,可是這種方案須要要在js內循環建立多個dom元素,既不優雅也影響性能,因而考慮可不能夠不生成這麼多個元素。canvas

2. canvas輸出背景圖

第一步仍是在頁面上覆蓋一個固定定位的盒子,而後建立一個canvas畫布,繪製出一個水印區域,將這個水印經過toDataURL方法輸出爲一個圖片,將這個圖片設置爲盒子的背景圖,經過backgroud-repeat:repeat;樣式實現填滿整個屏幕的效果,簡單實現的代碼。後端

<!DOCTYPE html>
<html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div id="info" onclick="alert(1)" > 123 </div> <script> (function () { function __canvasWM({ container = document.body, width = '300px', height = '200px', textAlign = 'center', textBaseline = 'middle', font = "20px Microsoft Yahei", fillStyle = 'rgba(184, 184, 184, 0.6)', content = '水印', rotate = '45', zIndex = 10000 } = {}) { const args = arguments[0]; const canvas = document.createElement('canvas'); canvas.setAttribute('width', width); canvas.setAttribute('height', height); const ctx = canvas.getContext("2d"); ctx.textAlign = textAlign; ctx.textBaseline = textBaseline; ctx.font = font; ctx.fillStyle = fillStyle; ctx.rotate(Math.PI / 180 * rotate); ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2); const base64Url = canvas.toDataURL(); const __wm = document.querySelector('.__wm'); const watermarkDiv = __wm || document.createElement("div"); const styleStr = ` position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; z-index:${zIndex}; pointer-events:none; background-repeat:repeat; background-image:url('${base64Url}')`; watermarkDiv.setAttribute('style', styleStr); watermarkDiv.classList.add('__wm'); if (!__wm) { container.insertBefore(watermarkDiv, container.firstChild); } if (typeof module != 'undefined' && module.exports) { //CMD module.exports = __canvasWM; } else if (typeof define == 'function' && define.amd) { // AMD define(function () { return __canvasWM; }); } else { window.__canvasWM = __canvasWM; } })(); // 調用 __canvasWM({ content: '水印123' }); </script> </body> </html> 複製代碼

3. svg實現背景圖

與canvas生成背景圖的方法相似,只不過是生成背景圖的方法換成了經過svg生成,canvas的兼容性略好於svg。 兼容性對比:瀏覽器

canvas安全

image.png

svg

image.png

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <div id="info" onclick="alert(1)">
            123
        </div>
        <script> (function () { function __canvasWM({ container = document.body, width = '300px', height = '200px', textAlign = 'center', textBaseline = 'middle', font = "20px Microsoft Yahei", fillStyle = 'rgba(184, 184, 184, 0.6)', content = '水印', rotate = '45', zIndex = 10000, opacity = 0.3 } = {}) { const args = arguments[0]; const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}"> <text x="50%" y="50%" dy="12px" text-anchor="middle" stroke="#000000" stroke-width="1" stroke-opacity="${opacity}" fill="none" transform="rotate(-45, 120 120)" style="font-size: ${font};"> ${content} </text> </svg>`; const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`; const __wm = document.querySelector('.__wm'); const watermarkDiv = __wm || document.createElement("div"); const styleStr = ` position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; z-index:${zIndex}; pointer-events:none; background-repeat:repeat; background-image:url('${base64Url}')`; watermarkDiv.setAttribute('style', styleStr); watermarkDiv.classList.add('__wm'); if (!__wm) { container.style.position = 'relative'; container.insertBefore(watermarkDiv, container.firstChild); } if (typeof module != 'undefined' && module.exports) { //CMD module.exports = __canvasWM; } else if (typeof define == 'function' && define.amd) { // AMD define(function () { return __canvasWM; }); } else { window.__canvasWM = __canvasWM; } })(); // 調用 __canvasWM({ content: '水印123' }); </script>
    </body>
</html>

複製代碼

可是,以上三種方法存在一個共同的問題,因爲是前端生成dom元素覆蓋到頁面上的,對於有些前端知識的人來講,能夠在開發者工具中找到水印所在的元素,將元素整個刪掉,以達到刪除頁面上的水印的目的,針對這個問題,我想到了一個很笨的辦法:設置定時器,每隔幾秒檢驗一次咱們的水印元素還在不在,有沒有被修改,若是發生了變化則再執行一次覆蓋水印的方法。網上看到了另外一種解決方法:使用MutationObserver

MutationObserver是變更觀察器,字面上就能夠理解這是用來觀察節點變化的。Mutation Observer API 用來監視 DOM 變更,DOM 的任何變更,好比子節點的增減、屬性的變更、文本內容的變更,這個 API 均可以獲得通知。

可是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
  })
}

}
複製代碼

總體代碼

<!DOCTYPE html>
<html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div id="info" onclick="alert(1)"> 123 </div> <script> (function () { function __canvasWM({ container = document.body, width = '300px', height = '200px', textAlign = 'center', textBaseline = 'middle', font = "20px Microsoft Yahei", fillStyle = 'rgba(184, 184, 184, 0.6)', content = '水印', rotate = '45', zIndex = 10000 } = {}) { const args = arguments[0]; const canvas = document.createElement('canvas'); canvas.setAttribute('width', width); canvas.setAttribute('height', height); const ctx = canvas.getContext("2d"); ctx.textAlign = textAlign; ctx.textBaseline = textBaseline; ctx.font = font; ctx.fillStyle = fillStyle; ctx.rotate(Math.PI / 180 * rotate); ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2); const base64Url = canvas.toDataURL(); const __wm = document.querySelector('.__wm'); const watermarkDiv = __wm || document.createElement("div"); const styleStr = ` position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; z-index:${zIndex}; pointer-events:none; background-repeat:repeat; background-image:url('${base64Url}')`; watermarkDiv.setAttribute('style', styleStr); watermarkDiv.classList.add('__wm'); if (!__wm) { container.style.position = 'relative'; container.insertBefore(watermarkDiv, container.firstChild); } 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 }) } } if (typeof module != 'undefined' && module.exports) { //CMD module.exports = __canvasWM; } else if (typeof define == 'function' && define.amd) { // AMD define(function () { return __canvasWM; }); } else { window.__canvasWM = __canvasWM; } })(); // 調用 __canvasWM({ content: '水印123' }); </script> </body> </html> 複製代碼

固然,設置了MutationObserver以後也只是相對安全了一些,仍是能夠經過控制檯禁用js來跳過咱們的監聽,整體來講在單純的在前端頁面上加水印老是能夠經過一些騷操做來跳過的,防君子不防小人,防外行不防內行

image.png

4. 圖片加水印

有時咱們須要在圖片上加水印用來標示歸屬或者其餘信息,在圖片上加水印的實現思路是,圖片加載成功後畫到canvas中,隨後在canvas中繪製水印,完成後經過canvas.toDataUrl()方法得到base64並替換原來的圖片路徑

代碼實現:

<!DOCTYPE html>
<html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div id="info" onclick="alert(1)"> <img /> </div> <script> (function() { function __picWM({ url = '', textAlign = 'center', textBaseline = 'middle', font = "20px Microsoft Yahei", fillStyle = 'rgba(184, 184, 184, 0.8)', content = '水印', cb = null, textX = 100, textY = 30 } = {}) { const img = new Image(); img.src = url; img.crossOrigin = 'anonymous'; img.onload = function() { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); ctx.textAlign = textAlign; ctx.textBaseline = textBaseline; ctx.font = font; ctx.fillStyle = fillStyle; ctx.fillText(content, img.width - textX, img.height - textY); const base64Url = canvas.toDataURL(); cb && cb(base64Url); } } if (typeof module != 'undefined' && module.exports) { //CMD module.exports = __picWM; } else if (typeof define == 'function' && define.amd) { // AMD define(function () { return __picWM; }); } else { window.__picWM = __picWM; } })(); // 調用 __picWM({ url: './a.png', content: '水印水印', cb: (base64Url) => { document.querySelector('img').src = base64Url }, }); </script> </body> </html> 複製代碼

5. 拓展:圖片的隱性水印

對於圖片資源來講,顯性水印會破壞圖片的完整性,有些狀況下咱們想要在保留圖片本來樣式,這時能夠添加隱藏水印。

簡單實現思路是:圖片的像素信息裏存儲着 RGB 的色值,對於RGB 份量值的小量變更,是肉眼沒法分辨的,不會影響對圖片的識別,咱們能夠對圖片的RGB以一種特殊規則進行小量的改動。

經過canvas.getImageData()能夠獲取到圖片的像素數據,首先在canvas中繪製出水印圖,獲取到其像素數據,而後經過canvas獲取到原圖片的像素數據,選定R、G、B其中一個如G,遍歷原圖片像素,將對應水印像素有信息的像素的G都轉成奇數,對應水印像素沒有信息的像素都轉成偶數,處理完後轉成base64並替換到頁面上,這時隱形水印就加好了,正常狀況下看這個圖片是沒有水印的,可是通過對應規則(上邊例子對應的解密規則是:遍歷圖片的像素數據中對應的G,奇數則將其rgba設置爲0,255,0,偶數則設置爲0,0,0)的解密處理後就能夠看到水印了。

這種方式下,當用戶採用截圖、保存圖片後轉換格式等方法得到圖片後,圖片的色值多是會變化的,會影響水印效果 加水印代碼實現:

<!DOCTYPE html>
<html> <head> <meta charset="utf-8"> <title></title> </head> <body> <canvas id="canvasText" width="256" height="256"></canvas> <canvas id="canvas" width="256" height="256"></canvas> <script> var ctx = document.getElementById('canvas').getContext('2d'); var ctxText = document.getElementById('canvasText').getContext('2d'); var textData; ctxText.font = '30px Microsoft Yahei'; ctxText.fillText('水印', 60, 130); textData = ctxText.getImageData(0, 0, ctxText.canvas.width, ctxText.canvas.height).data; var img = new Image(); var originalData; img.onload = function() { ctx.drawImage(img, 0, 0); // 獲取指定區域的canvas像素信息 originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); console.log(originalData); mergeData(textData,'G') console.log(document.getElementById('canvas').toDataURL()) }; img.src = './aa.jpeg'; var mergeData = function(newData, color){ var oData = originalData.data; var bit, offset; switch(color){ case 'R': bit = 0; offset = 3; break; case 'G': bit = 1; offset = 2; break; case 'B': bit = 2; offset = 1; break; } for(var i = 0; i < oData.length; i++){ if(i % 4 == bit){ // 只處理目標通道 if(newData[i + offset] === 0 && (oData[i] % 2 === 1)){ // 沒有水印信息的像素,將其對應通道的值設置爲偶數 if(oData[i] === 255){ oData[i]--; } else { oData[i]++; } } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)){ // 有水印信息的像素,將其對應通道的值設置爲奇數 if(oData[i] === 255){ oData[i]--; } else { oData[i]++; } } } } ctx.putImageData(originalData, 0, 0); } </script> </body> </html> 複製代碼

顯示水印代碼實現:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <canvas id="canvas" width="256" height="256"></canvas>
        
        <script> var ctx = document.getElementById('canvas').getContext('2d'); var img = new Image(); var originalData; img.onload = function() { ctx.drawImage(img, 0, 0); // 獲取指定區域的canvas像素信息 originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); console.log(originalData); processData(originalData) }; img.src = './a.jpg'; var processData = function(originalData){ var data = originalData.data; for(var i = 0; i < data.length; i++){ if(i % 4 == 1){ if(data[i] % 2 === 0){ data[i] = 0; } else { data[i] = 255; } } else if(i % 4 === 3){ // alpha通道不作處理 continue; } else { // 關閉其餘份量,不關閉也不影響答案,甚至更美觀 o(^▽^)o data[i] = 0; } } // 將結果繪製到畫布 ctx.putImageData(originalData, 0, 0); } </script>
    </body>
</html>


複製代碼

這是一種比較簡單的實現方式,有興趣想要了解更多的能夠參看juejin.cn/post/691793…

4、參考文檔

1.盲水印和圖片隱寫術:juejin.cn/post/691793…

2.不能說的祕密-前端也能玩的圖片隱寫術:www.alloyteam.com/2016/03/ima…

3.前端水印生成方案(網頁水印+圖片水印):juejin.cn/post/684490…

相關文章
相關標籤/搜索