15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > 前端水印实现方案

前端水印实现方案

时间:2023-07-21 08:06:01 | 来源:网站运营

时间:2023-07-21 08:06:01 来源:网站运营

前端水印实现方案:本文作者:ELab.liujinbo,原文链接:前端水印实现方案 (juejin.cn)

一、问题背景

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

前端浏览器加水印:

后端服务器加水印:

二、收益分析

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

三、实现方案

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) { //CMD module.exports = __canvasWM; } else if (typeof define == 'function' && define.amd) { // AMD define(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) { //CMD module.exports = __canvasWM; } else if (typeof define == 'function' && define.amd) { // AMD define(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元素变动才重新调用 __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 })}}整体代码

<!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元素变动才重新调用 __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: '水印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) { //CMD module.exports = __picWM; } else if (typeof define == 'function' && define.amd) { // AMD define(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(^▽^)o data[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







点击标题,查看往期优质文章

现代化 React 路由 Hookrouter 的使用

520,学废 new 对象的过程

iOS技能拓展 多环境配置

JavaScript 的静态作用域链与“动态”闭包链

我给网站做了一场性能手术

「前端性能」避免回流和重绘的必要性

「React进阶」 React全部api解读+基础实践大全(夯实基础2万字总结)

JavaScript 中如何实现大文件并行下载?

「react进阶」一文吃透react事件系统原理

关注掘金开发者社区公众号,了解更多技术干货~



关键词:方案,实现,水印

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭