前端web頁面防截屏水印生成方案(網頁水印+圖片水印)


前端水印生成方案


    前段時間做某系統審覈後臺,出現了審覈人員截圖把內容外部扭曲的情況,雖然截圖內容不是特別敏感,但是安全問題還是不能忽略。於是便在系統頁面上面加上了水印,對於審覈人員截圖等敏感操作有一定的提示作用。

網頁水印生成解決方案

通過canvas生成水印
畫布兼容性


這裏我們用canvas來生成base64圖片,通過CanIUse網站查詢兼容性,如果在移動端以及一些管理系統使用,兼容性問題可以完全忽略。

HTMLCanvasElement.toDataURL 方法返回一個包含圖片展示的數據URI。可以使用類型參數其類型,爲PNG格式。圖片的分辨率爲96dpi。

如果畫布的高度或寬度爲0,那麼會返回字符串“ data :,”。
如果初始化的類型非“ image / png”,但返回的值以“ data:image / png”開頭,那麼該必然的類型是不支持的。
鉻支持“圖像/ WEBP”類型。具體參考HTMLCanvasElement.toDataURL

具體代碼實現如下:

 (function () {
      // canvas 實現 watermark
      function __canvasWM({
        // 使用 ES6 的函數默認值方式設置參數的默認取值
        // 具體參見 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
        container = document.body,
        width = '200px',
        height = '150px',
        textAlign = 'center',
        textBaseline = 'middle',
        font = "20px microsoft yahei",
        fillStyle = 'rgba(184, 184, 184, 0.8)',
        content = '請勿外傳',
        rotate = '30',
        zIndex = 1000
      } = {}) {
        var args = arguments[0];
        var canvas = document.createElement('canvas');

        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        var 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);

        var base64Url = canvas.toDataURL();
        const watermarkDiv = document.createElement("div");
        watermarkDiv.setAttribute('style', `
          position:absolute;
          top:0;
          left:0;
          width:100%;
          height:100%;
          z-index:${zIndex};
          pointer-events:none;
          background-repeat:repeat;
          background-image:url('${base64Url}')`);

        container.style.position = 'relative';
        container.insertBefore(watermarkDiv, container.firstChild);

        
      });

      window.__canvasWM = __canvasWM;
    })();

    // 調用
    __canvasWM({
      content: 'QQMusicFE'
    })


效果如下:


![畫布實現網頁水印效果]

圖片

爲了使這個方法更通用,兼容不同的引用方式,我們還可以加上這段代碼:

   

  // 爲了兼容不同的環境
      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;
      }


這樣看起來能滿足我們的需求了,但是還有一個問題,稍微懂一點瀏覽器的使用或網頁知識的用戶,可以用瀏覽器的開發者工具來動態更改DOM的屬性或者結構就可以去掉了。當時有兩個解決辦法:

監測水印div的變化,記錄剛生成的div的innerHTML,每隔幾秒就取一次新的值,一旦發生變化,則重新生成水印。
使用MutationObserver
MutationObserver給開發者們提供了一種能在某個範圍內的DOM樹發生變化時做出適當反應的能力。

MutationObserver兼容性

圖片

通過兼容性表可以看出高級瀏覽器以及移動瀏覽器支持非常不錯。
突變觀察員API用來監視DOM變動.DOM的任何變動,比如節點的增減,屬性的變動,文本內容的變動,這個API都可以得到通知。
使用MutationObserver的實例的觀察函數方法用來啓動監聽,它接受兩個參數。
第一個參數:所要觀察的DOM節點,第二個參數:一個配置對象,指定所要觀察的特定變動,有以下幾種:

屬性 描述
childList 如果需要觀察目標目標的子例程(添加了某個子例程,或者可移除了某個子例程),則設置爲true。
屬性 如果需要觀察目標目標的屬性變量(添加或刪除了某個屬性,以及某個屬性的屬性值發生了變化),則設置爲true。
characterData 如果目標目標對象爲字符數據例程(一種抽象接口,具體可以爲文本索引,註釋索引,以及處理指令例程)時,也要觀察該例程的文本內容是否發生變化,則設置爲true。
子樹 除了目標例程,如果還需要觀察目標例程的所有後代例程,則設置爲true。
attributeOldValue 在屬性屬性已經被設置爲true,則需要設置爲true。
characterDataOldValue 在characterData屬性已經被設置爲true的情況下,如果需要將發生變化的characterData之前的文本內容記錄下來(記錄到下面的MutationRecord對象的oldValue屬性中),則設置爲true。
attributeFilter 一個屬性名數組(不需要指定命名空間),只有該數組中包含的屬性名發生變化時纔會被觀察到,其他名稱的屬性發生變化後會被忽略。


MutationObserver只能監測到某種屬性改變,增減子結點等,對於自己本身被刪除,是沒有辦法的可以通過監測父結點來達到要求。因此最終改造之後的代碼爲: 

 (function () {
      // canvas 實現 watermark
      function __canvasWM({
        // 使用 ES6 的函數默認值方式設置參數的默認取值
        // 具體參見 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
        container = document.body,
        width = '300px',
        height = '200px',
        textAlign = 'center',
        textBaseline = 'middle',
        font = "20px Microsoft Yahei",
        fillStyle = 'rgba(184, 184, 184, 0.6)',
        content = '請勿外傳',
        rotate = '30',
        zIndex = 1000
      } = {}) {
        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:absolute;
          top:0;
          left: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: 'QQMusicFE'
    });


通過SVG生成水印


SVG:可縮放矢量圖形(英語:Scalable Vector Graphics,SVG)是一種基於可擴展標記語言(XML),用於描述二維矢量圖形的圖形格式。維基百科

SVG瀏覽器兼容性

圖片

概述Canvas,SVG有更好的瀏覽器兼容性,使用SVG生成水印的方式與Canvas的方式類似,只是base64Url的生成方式換成SVG。具體如下:     

(function () {
      // svg 實現 watermark
      function __svgWM({
        container = document.body,
        content = '請勿外傳',
        width = '300px',
        height = '200px',
        opacity = '0.2',
        fontSize = '20px',
        zIndex = 1000
      } = {}) {
        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: ${fontSize};">
    ${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");
     // ...
     // 與 canvas 的一致
     // ...
    })();

    __svgWM({
      content: 'QQMusicFE'
    })


通過NodeJS生成水印


我們同樣可以通過NodeJS來生成網頁水印(出於性能考慮更好的方式是利用用戶客戶端來生成)。前端發一個請求,參數帶上水印內容,後臺返回圖片內容。
具體實現(Koa2環境):安裝gm以及相關環境,詳情看gm文檔
ctx.type = 'image/png';設置響應爲圖片類型
生成圖片過程是異步的,所以需要包裝一層Promise,這樣才能爲通過async / await方式爲ctx.body賦值

const fs = require('fs')
const gm = require('gm');
const imageMagick = gm.subClass({
  imageMagick: true
});


const router = require('koa-router')();

router.get('/wm', async (ctx, next) => {
  const {
    text
  } = ctx.query;

  ctx.type = 'image/png';
  ctx.status = 200;
  ctx.body = await ((() => {
    return new Promise((resolve, reject) => {
      imageMagick(200, 100, "rgba(255,255,255,0)")
        .fontSize(40)
        .drawText(10, 50, text)
        .write(require('path').join(__dirname, `./${text}.png`), function (err) {
          if (err) {
            reject(err);
          } else {
            resolve(fs.readFileSync(require('path').join(__dirname, `./${text}.png`)))
          }
        });
    })
  })());
});


如果只是簡單的水印展示,建議在瀏覽器生成,性能更好

圖片水印生成解決方案


除了給網頁加上水印之外,有時候我們需要給圖片也加上水印,這樣在用戶保存圖片後,帶上了水印來源信息,既可以保護版權,水印的其他信息也可以防止泄密。

通過畫布給圖片加水印
實現如下:   

 (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: 'http://localhost:3000/imgs/google.png',
        content: 'QQMusicFE',
        cb: (base64Url) => {
          document.querySelector('img').src = base64Url
        },
      });


效果如下:

帆布給圖片生成水印

圖片

通過NodeJS批量爲圖片加水印
我們同樣可以通過gm這個庫來給圖片加上水印

function picWM(path, text) {
  imageMagick(path)
    .drawText(10, 50, text)
    .write(require('path').join(__dirname, `./${text}.png`), function (err) {
      if (err) {
        console.log(err);
      }
    });
}


如果需要批處理圖片,只需要遍歷相關文件即可。

如果只是簡單的水印展示,建議在瀏覽器生成,性能更好

拓展   隱水印


前段時間阿里憑截圖查到了月餅事件的泄密者,其實就是用了隱水印這其實很大程度不是前端的範疇了,但是我們也應該瞭解.AlloyTeam團隊寫過一篇。不能說的祕密-前端也能玩的圖片隱寫術,通過畫布給圖片加上了“隱水印”,針對用戶保存的圖片,是可以輕鬆還原裏面隱含的內容,但是對於截圖或處理過的照片卻無能爲力,不過對於一些機密圖片文件展示,是可以偷偷用上該技術的。

使用加密後的水印內容
前端生成的水印也可以,別人也可以用同樣的方式生成,可能會有“嫁禍於人”(可能這是多慮的),我們還是要有更安全的解決方法。水印內容可以包含多種編碼後的信息,包括用戶名,用戶ID,時間等。例如我們只是想保存用戶唯一的用戶ID,需要把用戶ID放在下面的md5方法,就可以生成唯一標識。 ,但可以通過雙向遍歷所有用戶的方式進行後續。這樣就可以防止水印造假也可以進入真正水印的信息。

// MD5加密庫 utility
const utils = require('utility')

// 加鹽MD5
exports.md5 =  function (content) {
  const salt = 'microzz_asd!@#IdSDAS~~';
  return utils.md5(utils.md5(content + salt));
}


總結


安全問題不能大意,對於一些比較敏感的內容,我們可以通過組合使用上述的水印方案,這樣才能最大程度地給瀏覽者警示的作用,減少泄密的情況,甚至泄密了,也有可能追蹤到泄密者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章