導(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)頁面上面加上了水印,對于審核人員截圖等敏感操作有一定的提示作用。
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給開發(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),水印的其他信息也可以防止泄密。
實(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給圖片生成水印

我們同樣可以通過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ù)的。
前端生成的水印也可以,別人也可以用同樣的方式生成,可能會(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));}
安全問題不能大意,對于一些比較敏感的內(nèi)容,我們可以通過組合使用上述的水印方案,這樣才能最大程度給瀏覽者警示的作用,減少泄密的情況,即使泄密了,也有可能追蹤到泄密者。
lucifer-基于KM水印的圖片網(wǎng)頁水印實(shí)現(xiàn)方案
damon-網(wǎng)頁水印明水印前端SVG實(shí)現(xiàn)方案
參考:前端水印生成方案?--QQ音樂前端團(tuán)隊(duì)