前端水印生成方案

導語:前段時間作某系統審覈後臺,出現了審覈人員截圖把內容外泄露的狀況,雖然截圖內容不是特別敏感,可是安全問題仍是不能忽視。因而便在系統頁面上面加上了水印,對於審覈人員截圖等敏感操做有必定的提示做用。

前端水印生成方案

前段時間作某系統審覈後臺,出現了審覈人員截圖把內容外泄露的狀況,雖然截圖內容不是特別敏感,可是安全問題仍是不能忽視。因而便在系統頁面上面加上了水印,對於審覈人員截圖等敏感操做有必定的提示做用。javascript

網頁水印生成解決方案

經過canvas生成水印

Canvas兼容性html

圖片
這裏咱們用canvas來生成base64圖片,經過CanIUse網站查詢兼容性,若是在移動端以及一些管理系統使用,兼容性問題能夠徹底忽略。前端

HTMLCanvasElement.toDataURL 方法返回一個包含圖片展現的 data URI 。可使用 type 參數其類型,默認爲 PNG 格式。圖片的分辨率爲96dpi。java

若是畫布的高度或寬度是0,那麼會返回字符串「data:,」。
若是傳入的類型非「image/png」,可是返回的值以「data:image/png」開頭,那麼該傳入的類型是不支持的。
Chrome支持「image/webp」類型。具體參考HTMLCanvasElement.toDataURLnode

具體代碼實現以下:git

(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'
    })
複製代碼

效果以下:
![Canvas實現網頁水印效果]github

圖片

爲了使這個方法更通用,兼容不一樣的引用方式,咱們還能夠加上這段代碼:web

// 爲了兼容不一樣的環境
      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的屬性或者結構就能夠去掉了。這個時候有兩個解決辦法:canvas

  1. 監測水印div的變化,記錄剛生成的div的innerHTML,每隔幾秒就取一次新的值,一旦發生變化,則從新生成水印。可是這種方式可能影響性能;
  2. 使用MutationObserver

MutationObserver給開發者們提供了一種能在某個範圍內的DOM樹發生變化時做出適當反應的能力。數組

MutationObserver兼容性

圖片

經過兼容性表能夠看出高級瀏覽器以及移動瀏覽器支持很是不錯。
Mutation Observer API 用來監視 DOM 變更。DOM 的任何變更,好比節點的增減、屬性的變更、文本內容的變更,這個 API 均可以獲得通知。
使用MutationObserver構造函數,新建一個觀察器實例,實例的有一個回調函數,該回調函數接受兩個參數,第一個是變更數組,第二個是觀察器實例。MutationObserver 的實例的observe方法用來啓動監聽,它接受兩個參數。
第一個參數:所要觀察的 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只能監測到諸如屬性改變、增刪子結點等,對於本身自己被刪除,是沒有辦法的能夠經過監測父結點來達到要求。所以最終改造以後代碼爲:

(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由W3C制定,是一個開放標準。 -- 維基百科

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生成水印

身爲現代前端開發者,Node.JS也是須要掌握的。咱們一樣能夠經過NodeJS來生成網頁水印(出於性能考慮更好的方式是利用用戶客戶端來生成)。前端發一個請求,參數帶上水印內容,後臺返回圖片內容。
具體實現(Koa2環境):

  1. 安裝gm以及相關環境,詳情看gm文檔
  2. ctx.type = 'image/png';設置響應爲圖片類型
  3. 生成圖片過程是異步的,因此須要包裝一層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`)))
          }
        });
    })
  })());
});
複製代碼

若是隻是簡單的水印展現,建議在瀏覽器生成,性能更好

圖片水印生成解決方案

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

經過canvas給圖片加水印

實現以下:

(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
        },
      });
複製代碼

效果以下:

Canvas給圖片生成水印

圖片

經過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團隊寫過一篇 不能說的祕密——前端也能玩的圖片隱寫術 ,經過Canvas給圖片加上了「隱水印」,針對用戶保存的圖片,是能夠輕鬆還原裏面隱含的內容,可是對於截圖或者處理過的照片卻無能爲力,不過對於一些機密圖片文件展現,是能夠偷偷用上該技術的。

使用加密後的水印內容

前端生成的水印也能夠,別人也能夠用一樣的方式生成,可能會有「嫁禍於人」(可能這是多慮的),咱們仍是要有更安全的解決方法。水印內容能夠包含多種編碼後的信息,包括用戶名、用戶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));
}
複製代碼

總結

安全問題不能大意,對於一些比較敏感的內容,咱們能夠經過組合使用上述的水印方案,這樣才能最大程度給瀏覽者警示的做用,減小泄密的狀況,即便泄密了,也有可能追蹤到泄密者。

參考連接

  1. 不能說的祕密——前端也能玩的圖片隱寫術

  2. 阮一峯-Mutation Observer API

  3. lucifer-基於KM水印的圖片網頁水印實現方案

  4. damon-網頁水印明水印前端SVG實現方案

相關文章
相關標籤/搜索