前端水印生成

导语:前段时间做某系统审核后台,出现了审核人员截图把内容外泄露的情况,虽然截图内容不是特别敏感,但是安全问题还是不能忽视。于是便在系统页面上面加上了水印,对于审核人员截图等敏感操作有一定的提示作用。

前端水印生成方案

    前段时间做某系统审核后台,出现了审核人员截图把内容外泄露的情况,虽然截图内容不是特别敏感,但是安全问题还是不能忽视。于是便在系统页面上面加上了水印,对于审核人员截图等敏感操作有一定的提示作用。

网页水印生成解决方案

通过canvas生成水印

Canvas兼容性

这里我们用canvas来生成base64图片,通过CanIUse网站查询兼容性,如果在移动端以及一些管理系统使用,兼容性问题可以完全忽略。

HTMLCanvasElement.toDataURL 方法返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。

如果画布的高度或宽度是0,那么会返回字符串“data:,”。

如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。

Chrome支持“image/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(typeofmodule!='undefined'&&module.exports) {//CMDmodule.exports = __canvasWM; }elseif(typeofdefine =='function'&& define.amd) {// AMDdefine(function(){return__canvasWM; }); }else{window.__canvasWM = __canvasWM; }

这样似乎能满足我们的需求了,但是还有一个问题,稍微懂一点浏览器的使用或者网页知识的用户,可以用浏览器的开发者工具来动态更改DOM的属性或者结构就可以去掉了。这个时候有两个解决办法:

监测水印div的变化,记录刚生成的div的innerHTML,每隔几秒就取一次新的值,一旦发生变化,则重新生成水印。但是这种方式可能影响性能;

使用MutationObserver

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力。

MutationObserver兼容性


通过兼容性表可以看出高级浏览器以及移动浏览器支持非常不错。

Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

使用MutationObserver构造函数,新建一个观察器实例,实例的有一个回调函数,该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例。MutationObserver 的实例的observe方法用来启动监听,它接受两个参数。

第一个参数:所要观察的 DOM 节点,第二个参数:一个配置对象,指定所要观察的特定变动,有以下几种:


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

身为现代前端开发者,Node.JS也是需要掌握的。我们同样可以通过NodeJS来生成网页水印(出于性能考虑更好的方式是利用用户客户端来生成)。前端发一个请求,参数带上水印内容,后台返回图片内容。

具体实现(Koa2环境):

安装gm以及相关环境,详情看gm文档

ctx.type = 'image/png';设置响应为图片类型

生成图片过程是异步的,所以需要包装一层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`)))          }        });    })  })());});

如果只是简单的水印展示,建议在浏览器生成,性能更好

图片水印生成解决方案

除了给网页加上水印之外,有时候我们需要给图片也加上水印,这样在用户保存图片后,带上了水印来源信息,既可以保护版权,水印的其他信息也可以防止泄密。

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

效果如下:

Canvas给图片生成水印

通过NodeJS批量为图片加水印

我们同样可以通过gm这个库来给图片加上水印

functionpicWM(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加密库 utilityconstutils =require('utility')// 加盐MD5exports.md5 =function(content){constsalt ='microzz_asd!@#IdSDAS~~';returnutils.md5(utils.md5(content + salt));}

总结

安全问题不能大意,对于一些比较敏感的内容,我们可以通过组合使用上述的水印方案,这样才能最大程度给浏览者警示的作用,减少泄密的情况,即使泄密了,也有可能追踪到泄密者。

参考链接

不能说的秘密——前端也能玩的图片隐写术

阮一峰-Mutation Observer API

lucifer-基于KM水印的图片网页水印实现方案

damon-网页水印明水印前端SVG实现方案

参考:前端水印生成方案 --QQ音乐前端团队

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 先看下效果: 思路1: 使用canvas进行生成图片,然后动态生成div填充整个背景,将生成的图片用与 backg...
    wwmin_阅读 13,055评论 0 54
  • 一:canvas简介 1.1什么是canvas? ①:canvas是HTML5提供的一种新标签 ②:HTML5 ...
    GreenHand1阅读 4,663评论 2 32
  • 啥是canvas? HTML5 标签用于绘制图像(通过脚本,通常是 JavaScript)。不过, 元素本身...
    kiaizi阅读 760评论 0 4
  • 在项目中,需要生成海报。有动态信息(微信头像、微信昵称、上传图片(oss链接)、二维码)+ 海报背景图生成一张海报...
    奔跑吧笨笨阅读 7,659评论 0 0
  • 一、canvas简介 1.1 什么是canvas?(了解) 是HTML5提供的一种新标签 Canvas是一个矩形区...
    Looog阅读 3,936评论 3 40