前端水印生成

導(dǎo)語:前段時(shí)間做某系統(tǒng)審核后臺(tái),出現(xiàn)了審核人員截圖把內(nèi)容外泄露的情況,雖然截圖內(nèi)容不是特別敏感,但是安全問題還是不能忽視。于是便在系統(tǒng)頁面上面加上了水印,對于審核人員截圖等敏感操作有一定的提示作用。

前端水印生成方案

????前段時(shí)間做某系統(tǒng)審核后臺(tái),出現(xiàn)了審核人員截圖把內(nèi)容外泄露的情況,雖然截圖內(nèi)容不是特別敏感,但是安全問題還是不能忽視。于是便在系統(tǒng)頁面上面加上了水印,對于審核人員截圖等敏感操作有一定的提示作用。

網(wǎng)頁水印生成解決方案

通過canvas生成水印

Canvas兼容性

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

HTMLCanvasElement.toDataURL?方法返回一個(gè)包含圖片展示的 data URI ??梢允褂?type 參數(shù)其類型,默認(rèn)為 PNG 格式。圖片的分辨率為96dpi。

如果畫布的高度或?qū)挾仁?,那么會(huì)返回字符串“data:,”。

如果傳入的類型非“image/png”,但是返回的值以“data:image/png”開頭,那么該傳入的類型是不支持的。

Chrome支持“image/webp”類型。具體參考HTMLCanvasElement.toDataURL

具體代碼實(shí)現(xiàn)如下:


(function () {

? ? ? // canvas 實(shí)現(xiàn) watermark

? ? ? function __canvasWM({

? ? ? ? // 使用 ES6 的函數(shù)默認(rèn)值方式設(shè)置參數(shù)的默認(rèn)取值

? ? ? ? // 具體參見 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;

? ? })();

? ? // 調(diào)用

? ? __canvasWM({

? ? ? content: 'QQMusicFE'

? ? })


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

// 為了兼容不同的環(huán)境if(typeofmodule!='undefined'&&module.exports) {//CMDmodule.exports = __canvasWM; }elseif(typeofdefine =='function'&& define.amd) {// AMDdefine(function(){return__canvasWM; }); }else{window.__canvasWM = __canvasWM; }

這樣似乎能滿足我們的需求了,但是還有一個(gè)問題,稍微懂一點(diǎn)瀏覽器的使用或者網(wǎng)頁知識(shí)的用戶,可以用瀏覽器的開發(fā)者工具來動(dòng)態(tài)更改DOM的屬性或者結(jié)構(gòu)就可以去掉了。這個(gè)時(shí)候有兩個(gè)解決辦法:

監(jiān)測水印div的變化,記錄剛生成的div的innerHTML,每隔幾秒就取一次新的值,一旦發(fā)生變化,則重新生成水印。但是這種方式可能影響性能;

使用MutationObserver

MutationObserver給開發(fā)者們提供了一種能在某個(gè)范圍內(nèi)的DOM樹發(fā)生變化時(shí)作出適當(dāng)反應(yīng)的能力。

MutationObserver兼容性


通過兼容性表可以看出高級瀏覽器以及移動(dòng)瀏覽器支持非常不錯(cuò)。

Mutation Observer API 用來監(jiān)視 DOM 變動(dòng)。DOM 的任何變動(dòng),比如節(jié)點(diǎn)的增減、屬性的變動(dòng)、文本內(nèi)容的變動(dòng),這個(gè) API 都可以得到通知。

使用MutationObserver構(gòu)造函數(shù),新建一個(gè)觀察器實(shí)例,實(shí)例的有一個(gè)回調(diào)函數(shù),該回調(diào)函數(shù)接受兩個(gè)參數(shù),第一個(gè)是變動(dòng)數(shù)組,第二個(gè)是觀察器實(shí)例。MutationObserver 的實(shí)例的observe方法用來啟動(dòng)監(jiān)聽,它接受兩個(gè)參數(shù)。

第一個(gè)參數(shù):所要觀察的 DOM 節(jié)點(diǎn),第二個(gè)參數(shù):一個(gè)配置對象,指定所要觀察的特定變動(dòng),有以下幾種:


MutationObserver只能監(jiān)測到諸如屬性改變、增刪子結(jié)點(diǎn)等,對于自己本身被刪除,是沒有辦法的可以通過監(jiān)測父結(jié)點(diǎn)來達(dá)到要求。因此最終改造之后代碼為:

? (function () {

? ? ? // canvas 實(shí)現(xiàn) watermark

? ? ? function __canvasWM({

? ? ? ? // 使用 ES6 的函數(shù)默認(rèn)值方式設(shè)置參數(shù)的默認(rèn)取值

? ? ? ? // 具體參見 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元素變動(dòng)才重新調(diào)用 __canvasWM

? ? ? ? ? ? if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {

? ? ? ? ? ? ? // 避免一直觸發(fā)

? ? ? ? ? ? ? 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;

? ? ? }

? ? })();

? ? // 調(diào)用

? ? __canvasWM({

? ? ? content: 'QQMusicFE'

? ? });

通過SVG生成水印

SVG:可縮放矢量圖形(英語:Scalable Vector Graphics,SVG)是一種基于可擴(kuò)展標(biāo)記語言(XML),用于描述二維矢量圖形的圖形格式。 SVG由W3C制定,是一個(gè)開放標(biāo)準(zhǔn)。 --?維基百科

SVG瀏覽器兼容性


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

(function(){// svg 實(shí)現(xiàn) watermarkfunction__svgWM({? ? ? ? container = document.body,? ? ? ? content ='請勿外傳',? ? ? ? width ='300px',? ? ? ? height ='200px',? ? ? ? opacity ='0.2',? ? ? ? fontSize ='20px',? ? ? ? zIndex =1000} = {}){constargs =arguments[0];constsvgStr =`? ${content}`;constbase64Url =`data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;const__wm =document.querySelector('.__wm');constwatermarkDiv = __wm ||document.createElement("div");// ...// 與 canvas 的一致// ...})();? ? __svgWM({content:'QQMusicFE'})

通過NodeJS生成水印

身為現(xiàn)代前端開發(fā)者,Node.JS也是需要掌握的。我們同樣可以通過NodeJS來生成網(wǎng)頁水印(出于性能考慮更好的方式是利用用戶客戶端來生成)。前端發(fā)一個(gè)請求,參數(shù)帶上水印內(nèi)容,后臺(tái)返回圖片內(nèi)容。

具體實(shí)現(xiàn)(Koa2環(huán)境):

安裝gm以及相關(guān)環(huán)境,詳情看gm文檔

ctx.type = 'image/png';設(shè)置響應(yīng)為圖片類型

生成圖片過程是異步的,所以需要包裝一層Promise,這樣才能為通過 async/await 方式為 ctx.body 賦值

constfs =require('fs')constgm =require('gm');constimageMagick = gm.subClass({imageMagick:true});constrouter =require('koa-router')();router.get('/wm',async(ctx, next) => {const{? ? text? } = ctx.query;? ctx.type ='image/png';? ctx.status =200;? ctx.body =await((() =>{returnnewPromise((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`)))? ? ? ? ? }? ? ? ? });? ? })? })());});

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

圖片水印生成解決方案

除了給網(wǎng)頁加上水印之外,有時(shí)候我們需要給圖片也加上水印,這樣在用戶保存圖片后,帶上了水印來源信息,既可以保護(hù)版權(quán),水印的其他信息也可以防止泄密。

通過canvas給圖片加水印

實(shí)現(xiàn)如下:

(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} = {}){constimg =newImage();? ? ? ? img.src = url;? ? ? ? img.crossOrigin ='anonymous';? ? ? ? img.onload =function(){constcanvas =document.createElement('canvas');? ? ? ? ? canvas.width = img.width;? ? ? ? ? canvas.height = img.height;constctx = 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);constbase64Url = canvas.toDataURL();? ? ? ? ? cb && cb(base64Url);? ? ? ? }? ? ? }if(typeofmodule!='undefined'&&module.exports) {//CMDmodule.exports = __picWM;? ? ? }elseif(typeofdefine =='function'&& define.amd) {// AMDdefine(function(){return__picWM;? ? ? ? });? ? ? }else{window.__picWM = __picWM;? ? ? }? ? ? ? ? })();// 調(diào)用__picWM({url:'http://localhost:3000/imgs/google.png',content:'QQMusicFE',cb:(base64Url) =>{document.querySelector('img').src = base64Url? ? ? ? },? ? ? });

效果如下:

Canvas給圖片生成水印

通過NodeJS批量為圖片加水印

我們同樣可以通過gm這個(gè)庫來給圖片加上水印

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

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

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

拓展

隱水印

前段時(shí)間阿里憑截圖查到了月餅事件的泄密者,其實(shí)就是用了隱水印。這其實(shí)很大程度不是前端的范疇了,但是我們也應(yīng)該了解。AlloyTeam團(tuán)隊(duì)寫過一篇?不能說的秘密——前端也能玩的圖片隱寫術(shù)?,通過Canvas給圖片加上了“隱水印”,針對用戶保存的圖片,是可以輕松還原里面隱含的內(nèi)容,但是對于截圖或者處理過的照片卻無能為力,不過對于一些機(jī)密圖片文件展示,是可以偷偷用上該技術(shù)的。

使用加密后的水印內(nèi)容

前端生成的水印也可以,別人也可以用同樣的方式生成,可能會(huì)有“嫁禍于人”(可能這是多慮的),我們還是要有更安全的解決方法。水印內(nèi)容可以包含多種編碼后的信息,包括用戶名、用戶ID、時(shí)間等。比如我們只是想保存用戶唯一的用戶ID,需要把用戶ID傳入下面的md5方法,就可以生成唯一標(biāo)識(shí)。編碼后的信息是不可逆的,但可以通過全局遍歷所有用戶的方式進(jìn)行追溯。這樣就可以防止水印造假也可以追溯真正水印的信息。

// MD5加密庫 utilityconstutils =require('utility')// 加鹽MD5exports.md5 =function(content){constsalt ='microzz_asd!@#IdSDAS~~';returnutils.md5(utils.md5(content + salt));}

總結(jié)

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

參考鏈接

不能說的秘密——前端也能玩的圖片隱寫術(shù)

阮一峰-Mutation Observer API

lucifer-基于KM水印的圖片網(wǎng)頁水印實(shí)現(xiàn)方案

damon-網(wǎng)頁水印明水印前端SVG實(shí)現(xiàn)方案

參考:前端水印生成方案?--QQ音樂前端團(tuán)隊(duì)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 先看下效果: 思路1: 使用canvas進(jìn)行生成圖片,然后動(dòng)態(tài)生成div填充整個(gè)背景,將生成的圖片用與 backg...
    wwmin_閱讀 13,389評論 0 54
  • 一:canvas簡介 1.1什么是canvas? ①:canvas是HTML5提供的一種新標(biāo)簽 ②:HTML5 ...
    GreenHand1閱讀 4,882評論 2 32
  • 啥是canvas? HTML5 標(biāo)簽用于繪制圖像(通過腳本,通常是 JavaScript)。不過, 元素本身...
    kiaizi閱讀 855評論 0 4
  • 在項(xiàng)目中,需要生成海報(bào)。有動(dòng)態(tài)信息(微信頭像、微信昵稱、上傳圖片(oss鏈接)、二維碼)+ 海報(bào)背景圖生成一張海報(bào)...
    奔跑吧笨笨閱讀 7,953評論 0 0
  • 一、canvas簡介 1.1 什么是canvas?(了解) 是HTML5提供的一種新標(biāo)簽 Canvas是一個(gè)矩形區(qū)...
    Looog閱讀 4,045評論 3 40

友情鏈接更多精彩內(nèi)容