一、问题背景

为了防止信息泄露或知识产权被侵犯,在web的世界里,对于页面和图片等增加水印处理是十分有必要的,水印的添加根据环境可以分为两大类,前端浏览器环境添加和后端服务环境添加,简单对比一下这两种方式的特点:

前端浏览器加水印:

  • 减轻服务端的压力,快速反应

  • 安全系数较低,对于掌握一定前端知识的人来说可以通过各种骚操作跳过水印获取到源文件

  • 适用场景:

    资源不跟某一个单独的用户绑定,而是一份资源,多个用户查看,需要在每一个用户查看的时候添加用户特有的水印,多用于某些机密文档或者展示机密信息的页面,水印的目的在于文档外流的时候可以追究到责任人

后端服务器加水印:

  • 当遇到大文件密集水印,或是复杂水印,占用服务器内存、运算量,请求时间过长

  • 安全性高,无法获取到加水印前的源文件

  • 适用场景:资源为某个用户独有,一份原始资源只需要做一次处理,将其存储之后就无需再次处理,水印的目的在于标示资源的归属人 这里我们讨论前端浏览器环境添加

二、收益分析

简单介绍一下目前主流的前端加水印的方法,以后其他同学在用到的时候可以作为参考。

回复“8”加入面试题分享群

三、实现方案

1. 重复的dom元素覆盖实现

从效果开始,要实现的效果是「在页面上充满透明度较低的重复的代表身份的信息」,第一时间想到的方案是在页面上覆盖一个position:fixed的div盒子,盒子透明度设置较低,设置pointer-events: none;样式实现点击穿透,在这个盒子内通过js循环生成小的水印div,每个水印div内展示一个要显示的水印内容,简单实现了一下

<!DOCTYPE html>
<html> <head> <meta charset="utf-8"> <title></title> <style> #watermark-box { position: fixed; top: 0; bottom: 0; left: 0; right: 0; font-size: 24px; font-weight: 700; display: flex; flex-wrap: wrap; overflow: hidden; user-select: none; pointer-events: none; opacity: 0.1; z-index: 999;} .watermark { text-align: center; } </style> </head> <body> <div> <h2> 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- </h2> <br /> <h2> 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- </h2> <br /> <h2 onclick="alert(1)"> 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- 机密内容- </h2> <br /> </div> <div id="watermark-box"> </div> <script> function doWaterMark(width, height, content) { let box = document.getElementById("watermark-box"); let boxWidth = box.clientWidth, boxHeight = box.clientHeight; for (let i = 0; i < Math.floor(boxHeight / height); i++) { for (let j = 0; j < Math.floor(boxWidth / width); j++) { let next = document.createElement("div") next.setAttribute("class", "watermark") next.style.width = width + 'px' next.style.height = height + 'px' next.innerText = content box.appendChild(next) } } } window.onload = doWaterMark(300, 100, '水印123') </script> </body>
</html>

页面效果是有了,但是这种方案需要要在js内循环创建多个dom元素,既不优雅也影响性能,于是考虑可不可以不生成这么多个元素。

2. canvas输出背景图

第一步还是在页面上覆盖一个固定定位的盒子,然后创建一个canvas画布,绘制出一个水印区域,将这个水印通过toDataURL方法输出为一个图片,将这个图片设置为盒子的背景图,通过backgroud-repeat:repeat;样式实现填满整个屏幕的效果,简单实现的代码。

 <!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title></head><body><div id="info" onclick="alert(1)" >123</div><script>(function () {function __canvasWM({container = document.body,width = '300px',height = '200px',textAlign = 'center',textBaseline = 'middle',font = "20px Microsoft Yahei",fillStyle = 'rgba(184, 184, 184, 0.6)',content = '水印',rotate = '45',zIndex = 10000} = {}) {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:fixed;top:0;left:0;bottom:0;right: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.insertBefore(watermarkDiv, container.firstChild);}if (typeof module != 'undefined' && module.exports) {  //CMDmodule.exports = __canvasWM;} else if (typeof define == 'function' && define.amd) { // AMDdefine(function () {return __canvasWM;});} else {window.__canvasWM = __canvasWM;}})();// 调用__canvasWM({content: '水印123'});</script></body>
</html>

3. svg实现背景图

与canvas生成背景图的方法类似,只不过是生成背景图的方法换成了通过svg生成,canvas的兼容性略好于svg。兼容性对比:

canvas


svg


<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title></head><body><div id="info" onclick="alert(1)">123</div><script>(function () {function __canvasWM({container = document.body,width = '300px',height = '200px',textAlign = 'center',textBaseline = 'middle',font = "20px Microsoft Yahei",fillStyle = 'rgba(184, 184, 184, 0.6)',content = '水印',rotate = '45',zIndex = 10000,opacity = 0.3} = {}) {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: ${font};">${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");const styleStr = `position:fixed;top:0;left:0;bottom:0;right: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);}if (typeof module != 'undefined' && module.exports) {  //CMDmodule.exports = __canvasWM;} else if (typeof define == 'function' && define.amd) { // AMDdefine(function () {return __canvasWM;});} else {window.__canvasWM = __canvasWM;}})();// 调用__canvasWM({content: '水印123'});</script></body>
</html>

但是,以上三种方法存在一个共同的问题,由于是前端生成dom元素覆盖到页面上的,对于有些前端知识的人来说,可以在开发者工具中找到水印所在的元素,将元素整个删掉,以达到删除页面上的水印的目的,针对这个问题,我想到了一个很笨的办法:设置定时器,每隔几秒检验一次我们的水印元素还在不在,有没有被修改,如果发生了变化则再执行一次覆盖水印的方法。网上看到了另一种解决方法:使用MutationObserver

MutationObserver是变动观察器,字面上就可以理解这是用来观察节点变化的。Mutation Observer API 用来监视 DOM 变动,DOM 的任何变动,比如子节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

但是MutationObserver只能监测到诸如属性改变、子结点变化等,对于自己本身被删除,是没有办法监听的,这里可以通过监测父结点来达到要求。监测代码的实现:

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {let mo = new MutationObserver(function () {const __wm = document.querySelector('.__wm');// 只在__wm元素变动才重新调用 __canvasWMif ((__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})
}}

整体代码

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title></head><body><div id="info" onclick="alert(1)">123</div><script>(function () {function __canvasWM({container = document.body,width = '300px',height = '200px',textAlign = 'center',textBaseline = 'middle',font = "20px Microsoft Yahei",fillStyle = 'rgba(184, 184, 184, 0.6)',content = '水印',rotate = '45',zIndex = 10000} = {}) {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:fixed;top:0;left:0;bottom:0;right: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元素变动才重新调用 __canvasWMif ((__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) {  //CMDmodule.exports = __canvasWM;} else if (typeof define == 'function' && define.amd) { // AMDdefine(function () {return __canvasWM;});} else {window.__canvasWM = __canvasWM;}})();// 调用__canvasWM({content: '水印123'});</script></body>
</html>

当然,设置了MutationObserver之后也只是相对安全了一些,还是可以通过控制台禁用js来跳过我们的监听,总体来说在单纯的在前端页面上加水印总是可以通过一些骚操作来跳过的,防君子不防小人,防外行不防内行


4. 图片加水印

有时我们需要在图片上加水印用来标示归属或者其他信息,在图片上加水印的实现思路是,图片加载成功后画到canvas中,随后在canvas中绘制水印,完成后通过canvas.toDataUrl()方法获得base64并替换原来的图片路径

代码实现:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title></head><body><div id="info" onclick="alert(1)"><img /></div><script>(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) {  //CMDmodule.exports = __picWM;} else if (typeof define == 'function' && define.amd) { // AMDdefine(function () {return __picWM;});} else {window.__picWM = __picWM;}})();// 调用__picWM({url: './a.png',content: '水印水印',cb: (base64Url) => {document.querySelector('img').src = base64Url},});</script></body>
</html>

5. 拓展:图片的隐性水印

对于图片资源来说,显性水印会破坏图片的完整性,有些情况下我们想要在保留图片原本样式,这时可以添加隐藏水印。

简单实现思路是:图片的像素信息里存储着 RGB 的色值,对于RGB 分量值的小量变动,是肉眼无法分辨的,不会影响对图片的识别,我们可以对图片的RGB以一种特殊规则进行小量的改动。

通过canvas.getImageData()可以获取到图片的像素数据,首先在canvas中绘制出水印图,获取到其像素数据,然后通过canvas获取到原图片的像素数据,选定R、G、B其中一个如G,遍历原图片像素,将对应水印像素有信息的像素的G都转成奇数,对应水印像素没有信息的像素都转成偶数,处理完后转成base64并替换到页面上,这时隐形水印就加好了,正常情况下看这个图片是没有水印的,但是经过对应规则(上边例子对应的解密规则是:遍历图片的像素数据中对应的G,奇数则将其rgba设置为0,255,0,偶数则设置为0,0,0)的解密处理后就可以看到水印了。

这种方式下,当用户采用截图、保存图片后转换格式等方法获得图片后,图片的色值可能是会变化的,会影响水印效果 加水印代码实现:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title></head><body><canvas id="canvasText" width="256" height="256"></canvas><canvas id="canvas" width="256" height="256"></canvas><script>var ctx = document.getElementById('canvas').getContext('2d');var ctxText = document.getElementById('canvasText').getContext('2d');var textData;ctxText.font = '30px Microsoft Yahei';ctxText.fillText('水印', 60, 130);textData = ctxText.getImageData(0, 0, ctxText.canvas.width, ctxText.canvas.height).data;var img = new Image();var originalData;img.onload = function() {ctx.drawImage(img, 0, 0);// 获取指定区域的canvas像素信息originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);console.log(originalData);mergeData(textData,'G')console.log(document.getElementById('canvas').toDataURL())};img.src = './aa.jpeg';var mergeData = function(newData, color){var oData = originalData.data;var bit, offset;  switch(color){case 'R':bit = 0;offset = 3;break;case 'G':bit = 1;offset = 2;break;case 'B':bit = 2;offset = 1;break;}for(var i = 0; i < oData.length; i++){if(i % 4 == bit){// 只处理目标通道if(newData[i + offset] === 0 && (oData[i] % 2 === 1)){// 没有水印信息的像素,将其对应通道的值设置为偶数if(oData[i] === 255){oData[i]--;} else {oData[i]++;}} else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)){// 有水印信息的像素,将其对应通道的值设置为奇数if(oData[i] === 255){oData[i]--;} else {oData[i]++;}}}}ctx.putImageData(originalData, 0, 0);}</script></body>
</html>

显示水印代码实现:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title></head><body><canvas id="canvas" width="256" height="256"></canvas><script>var ctx = document.getElementById('canvas').getContext('2d');var img = new Image();var originalData;img.onload = function() {ctx.drawImage(img, 0, 0);// 获取指定区域的canvas像素信息originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);console.log(originalData);processData(originalData)};img.src = './a.jpg';var processData = function(originalData){var data = originalData.data;for(var i = 0; i < data.length; i++){if(i % 4 == 1){if(data[i] % 2 === 0){data[i] = 0;} else {data[i] = 255;}} else if(i % 4 === 3){// alpha通道不做处理continue;} else {// 关闭其他分量,不关闭也不影响答案,甚至更美观 o(^▽^)odata[i] = 0;}}// 将结果绘制到画布ctx.putImageData(originalData, 0, 0);}</script></body>
</html>

这是一种比较简单的实现方式,有兴趣想要了解更多的可以参看https://juejin.cn/post/6917934964202242061

四、参考文档

1.盲水印和图片隐写术:https://juejin.cn/post/6917934964202242061

2.不能说的秘密-前端也能玩的图片隐写术:http://www.alloyteam.com/2016/03/image-steganography/

3.前端水印生成方案(网页水印+图片水印):https://juejin.cn/post/6844903645155164174

1. JavaScript 重温系列(22篇全)

2. ECMAScript 重温系列(10篇全)

3. JavaScript设计模式 重温系列(9篇全)

4. 正则 / 框架 / 算法等 重温系列(16篇全)

5. Webpack4 入门(上)|| Webpack4 入门(下)

6. MobX 入门(上) ||  MobX 入门(下)

7. 120+篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看 120+ 篇原创文章

【Web技术】前端水印实现方案相关推荐

  1. 前端水印生成方案(网页水印+图片水印)

    参考链接 不能说的秘密--前端也能玩的图片隐写术 阮一峰-Mutation Observer API lucifer-基于KM水印的图片网页水印实现方案 damon-网页水印明水印前端SVG实现方案 ...

  2. 【WEB】前端系统配色方案(全览)

    写在前面的 暖色篇 过渡篇 冷色篇 总结 写在前面的     首先,这篇文章是WEB前端设计中经常用到的比较好看的一些颜色,当然对色盲或者色弱的小伙伴们来说那是相当不友好.因为,即使是色觉正常的人来讲 ...

  3. 前端web页面防截屏水印生成方案(网页水印+图片水印)

    前端水印生成方案 前段时间做某系统审核后台,出现了审核人员截图把内容外部扭曲的情况,虽然截图内容不是特别敏感,但是安全问题还是不能忽略.于是便在系统页面上面加上了水印,对于审核人员截图等敏感操作有一定 ...

  4. Web开发种色系搭配方案和常用颜色码

    在进行web开发或者小程序.APP时,如何获得赏心悦目的效果,获得领导和客户的青睐是广大程序员的急需解决的大问题.鄙人在网上找了一些资料,希望能帮到入门新手.色彩是直接能影响人的心情,不同的作品主题就 ...

  5. 【Web技术】1517- 你知道前端水印功能是怎么实现的吗?

    作者:熊的猫 https://juejin.cn/post/7132620574198595597 前言 由于项目需要实现水印功能,于是去了解了相关的内容后,基于 Vue 的实现了一个 v-water ...

  6. 有关前端性能优化的方案—Vue 代码层面性能优化+Webpack 层面的优化+基础的 Web 技术优化+非框架代码优化

    文章目录: 一.代码层面的优化 1.1.v-if 和 v-show 区分使用场景 1.2.computed 和 watch 区分使用场景 1.3.v-for 遍历必须为 item 添加 key,且避免 ...

  7. 【Web技术】1295- 总结一下前端本地储存方案

    作者:星尘starx https://juejin.cn/post/6925311938419408904 引言 2022 年,如果你的前端应用,需要在浏览器上保存数据,有三个主流方案: Cookie ...

  8. 用WEB技术栈开发NATIVE应用(二):WEEX 前端SDK原理详解

    摘要: WEEX依旧采取传统的web开发技术栈进行开发,同时app在终端的运行体验不输native app.其同时解决了开发效率.发版速度以及用户体验三个核心问题.那么WEEX是如何实现的?目前WEE ...

  9. 前端web 技术盘点

    尽管前端技术在无线领域受到了挫折,但这无法减缓其发展势头.在基础技术方面,规范和标准的发展.浏览器的快速演进为将来的Web应用打好了根基:随着网站规模的进一步变大,交互变得更复杂,大家更关注用新的开发 ...

最新文章

  1. 我最喜欢的几个苏州美食
  2. Uva 10537 过路费
  3. 命名实参和可选实参(C#)
  4. 消费者关注的 Win8 问题汇总(中)
  5. 【小白学PyTorch】18.TF2构建自定义模型
  6. 【H.264/AVC视频编解码技术】第五章【哈夫曼编码】
  7. 如何使用Chrome的Network面板分析HTTP报文
  8. 玩转算法之面试第九章-动态规划
  9. 移动硬盘安装Windows7
  10. (22)进程和线程区别
  11. ShardingSphere RAW JDBC 分布式事务 Narayana XA 代码示例
  12. SLAM会议笔记(三)V-LOAM
  13. iOS开发之cocoapods安装(2017)
  14. 均值滤波器、中值滤波器、滤波器的常见应用。
  15. 吊打本地搜索神器everthing,最快 最强的电脑本地搜索神器!
  16. java 一元二次方程_java求解一元二次方程
  17. PDF如何旋转其中一页?
  18. 为什么要阅读《首先,打破一切常规》
  19. 仓库管理系统-新名词(经济订货批量 、订货周期、订货提前期)
  20. AMS1084电路图

热门文章

  1. 【二维前缀和】304. 二维区域和检索 - 矩阵不可变
  2. 机器人唱歌bgm_爱死亡与机器人 全剧歌单BGM
  3. 咽炎引发-----喉源性咳嗽(摘)
  4. LoRa模块无线通信技术在距离测量和定位上的应用——东胜物联
  5. HA 高可用软件系统保养指南
  6. OS-练习题(10~13)
  7. 电脑上的计算机可以加密码,如何给电脑上的文件夹加密
  8. latex06-LaTeX中的特殊字符
  9. node启动之后内存占用过高解决方案
  10. Mutex与Semaphore 第一部分:Semephore