一、背景
用户在使用系统的时候,有些数据是有权限的用户才能查看,有权限查看数据的用户在查看数据的时候,把数据截图发给了没有权限查看的用户,然后数据泄露了,当截图多次流转后就逐渐不知道最初是谁截的图,也就无从提醒截图的人要注意数据保密了。
前端水印技术,作为一种在网页上动态生成水印的解决方案,能够有效地在数据展示页面上添加个性化标记。此外,水印的存在本身也是一种警示,提醒每一位用户尊重数据隐私,遵守公司规定,共同维护企业信息安全。
二、水印实现方案
方案1:重复dom元素覆盖
在页面的body上插入一个position:fixed的div盒子,设置pointer-events: none; css实现盒子的点击穿透,在这个div内通过js循环生成小水印div,每个水印div内显示的水印内容,具体代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重复dom元素水印</title>
</head>
<style>
</style>
<body>
<div class="app">
请给我加上水印,谢谢
</div>
<script>
/**
* [createdWaterMask] 生成水印
* @params {number} width 单个水印的宽度
* @params {number} height 单个水印的高度
* @params {number} content 水印内容
*
* */
function createdWaterMask(width, height, content) {
// 生成水印节点
const waterMaskBox = document.createElement("div")
waterMaskBox.setAttribute("id", "waterMask")
// 设置水印盒子的样式,其中user-select,pointer-events是关键
waterMaskBox.style = `
position: fixed;
top: 0;
bottom: 0;
left: 0;
range: 0;
width: 100%;
height: 100%;
font-size: 18px;
font-weight: 600;
/* 控制水印dom布局 */
display: flex;
flex-wrap: wrap;
overflow: hidden;
/* 整个水印盒子不能操作 */
user-select: none;
pointer-events: none;
opacity: 0.3;
z-index: 9999;
`
// 把水印盒子插到body下
document.body.appendChild(waterMaskBox)
// 获取盒子的大小
let box = document.getElementById("waterMask");
let boxWidth = box.clientWidth,
boxHeight = box.clientHeight;
// 根据水印大小和水印盒子的大小(这里是视口大小),生成对应数量的dom元素,并插入
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.style.width = width + 'px'
next.style.height = height + 'px'
next.innerText = content
box.appendChild(next)
}
}
}
createdWaterMask(200, 200, '我是水印')
</script>
</body>
</html>
水印效果是实现了,但是由于该方案需要用js计算创建dom元素的数量和频繁创建,在视口修改的时候需要重复计算和操作水印的dom元素,在交互和性能上存在问题(切图仔也有追求,优雅永不过时),因此开始寻找其他方案。
方案2:通过canvas生成图片设置为背景
同理,第一步都是在body下插入一个fixed定位的div盒子并设置点透的css:pointer-events: none;,然后就是通过canvas绘画出单一的水印区域,并将该区域的内容转换成可以作为background-image值的base64格式的数据流,将该数据流设置为盒子的背景图,通过backgroud-repeat:repeat;样式完成整个屏幕的效果循环填充。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas 生成水印</title>
</head>
<style>
</style>
<body>
<div class="app">
我是网页
</div>
<script>
/**
* [createdCanvas] 通过canvas生成单个水印图片
* @params {number} width 单个水印的宽度
* @params {number} height 单个水印的高度
* @params {number} content 水印内容
*
**/
function createdCanvas(width, height, content) {
const canvas = document.createElement('canvas')
// 设置单个水印的宽高
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
// 设置canvas文本样式
ctx.font = '20px Microsoft Yahei';
ctx.fillStyle = 'rgba(184, 184, 184, 0.6)';
// 写入水印文案
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
const img = canvas.toDataURL()
return img
}
/**
* [createdWaterMask] 生成水印
* @params {number} width 单个水印的宽度
* @params {number} height 单个水印的高度
* @params {number} content 水印内容
*
* */
function createdWaterMask(width, height, content) {
// 生成水印节点
const waterMaskBox = document.createElement("div")
waterMaskBox.setAttribute("id", "waterMask")
// 获取对应的水印图片
const base64Url = createdCanvas(width, height, content)
waterMaskBox.style = `
position: fixed;
top: 0;
bottom: 0;
left: 0;
range: 0;
width: 100%;
height: 100%;
font-size: 18px;
font-weight: 600;
/* 控制水印dom布局 */
display: flex;
flex-wrap: wrap;
overflow: hidden;
/* 整个水印盒子不能操作 */
user-select: none;
pointer-events: none;
opacity: 1;
z-index: 9999;
pointer-events:none;
/*设置背景平铺和背景图*/
background-repeat:repeat;
background-image:url('${base64Url}');
`
// 把水印盒子插到body下
document.body.appendChild(waterMaskBox)
}
createdWaterMask(300, 300, '我是水印')
</script>
</body>
</html>
该方案也是目前实现网页水印最常用的方法之一,不管是水印文案和水印的显示,都能灵活的通过参数来实现。
方案3:利用svg作为背景实现
和canvas的方案基本上相似,唯一的区别就是水印div的背景图不是直接生成的了,而是直接用svg来代替。在兼容性上而言,svg的兼容性会比vanvas的差一点。
Canvas兼容性图:
SVG兼容性图:
实现方案如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas 生成水印</title>
</head>
<style>
</style>
<body>
<div class="app">
我是网页
</div>
<script>
/**
* [createdWaterMask] 生成水印
* @params {number} width 单个水印的宽度
* @params {number} height 单个水印的高度
* @params {number} content 水印内容
*
* */
function createdWaterMask(width, height, content) {
// 生成水印节点
const waterMaskBox = document.createElement("div")
waterMaskBox.setAttribute("id", "waterMask")
// 获取对应的水印图片
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="0.3"
fill="none"
transform="rotate(-30, 120 120)">
${content}
</text>
</svg>`;
const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
waterMaskBox.style = `
position: fixed;
top: 0;
bottom: 0;
left: 0;
range: 0;
width: 100%;
height: 100%;
font-size: 18px;
font-weight: 600;
/* 控制水印dom布局 */
display: flex;
flex-wrap: wrap;
overflow: hidden;
/* 整个水印盒子不能操作 */
user-select: none;
pointer-events: none;
opacity: 1;
z-index: 9999;
pointer-events:none;
/*设置背景平铺和背景图*/
background-repeat:repeat;
background-image:url('${base64Url}');
`
// 把水印盒子插到body下
document.body.appendChild(waterMaskBox)
}
createdWaterMask(300, 300, '我是水印')
</script>
</body>
</html>
三、前端水印防护方案
在网页中添加水印是一项常见的版权保护措施。尽管目前有多种在前端生成并插入DOM元素的方法来实现水印功能,但它们都存在一个共同的弱点:对于具备一定前端知识的人来说,只需简单地查找并删除或隐藏页面中的相关DOM元素,便可轻松移除水印。为了克服这一缺陷,我们需要对页面进行实时监控。在这里,现代浏览器提供的MutationObserver API成为了一个理想的解决方案。它能够监控DOM树的任何变动,从而有效防止未经授权的DOM元素修改。作为旧的Mutation Events的替代品,MutationObserver是DOM3 Events规范的一部分,它为前端开发者提供了一种强大的手段来保护网页内容不被非法篡改。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas 生成水印</title>
</head>
<style>
</style>
<body>
<div class="app">
我是网页
</div>
<script>
/**
* [createdCanvas] 通过canvas生成单个水印图片
* @params {number} width 单个水印的宽度
* @params {number} height 单个水印的高度
* @params {number} content 水印内容
*
**/
function createdCanvas(width, height, content) {
const canvas = document.createElement('canvas')
// 设置单个水印的宽高
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
// 设置canvas文本样式
ctx.font = '20px Microsoft Yahei';
ctx.fillStyle = 'rgba(184, 184, 184, 0.6)';
// 写入水印文案
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
const img = canvas.toDataURL()
return img
}
/**
* [createdWaterMask] 生成水印
* @params {number} width 单个水印的宽度
* @params {number} height 单个水印的高度
* @params {number} content 水印内容
*
* */
function createdWaterMask(width, height, content) {
// 生成水印节点
const waterMaskBox = document.createElement("div")
waterMaskBox.setAttribute("id", "waterMask")
// 获取对应的水印图片
const base64Url = createdCanvas(width, height, content)
waterMaskBox.style = `
position: fixed;
top: 0;
bottom: 0;
left: 0;
range: 0;
width: 100%;
height: 100%;
border: 1px red solid;
font-size: 18px;
font-weight: 600;
/* 控制水印dom布局 */
display: flex;
flex-wrap: wrap;
overflow: hidden;
/* 整个水印盒子不能操作 */
user-select: none;
pointer-events: none;
opacity: 1;
z-index: 9999;
pointer-events:none;
/*设置背景平铺和背景图*/
background-repeat:repeat;
background-image:url('${base64Url}');
`
// 把水印盒子插到body下
document.body.appendChild(waterMaskBox)
}
createdWaterMask(300, 300, '我是水印')
// 记录水印的当前的节点信息
const firstWaterMaskEl = document.getElementById('waterMask')
// 水印节点样式
const firstWaterMaskStyle = firstWaterMaskEl?.getAttribute('style')
const observer = new MutationObserver(() => {
// 获取水印节点
const instance = document.getElementById('waterMask')
// 水印节点样式
const style = instance?.getAttribute(' style')
// 修改样式删除div
if ((instance && style !== firstWaterMaskStyle) || !instance) {
// 避免重复触发监听,这里先暂停
observer.disconnect()
if (instance) {
instance.setAttribute('style', firstWaterMaskStyle)
} else {
// div不在,说明删除了div
createdWaterMask(300, 300, '我是水印')
}
// 修改完重新开启监听
observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true,
})
}
})
// 启动监控
observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true,
})
</script>
</body>
</html>
到此就实现了网页水印和对水印节点的监听,但是即使如此也只是相对安全,毕竟这些数据和操作都是在客户端上实现,还是有很多方法可以绕过。只能防君子不能防小人。
作者介绍:
道一云七巧低代码开发平台,让零代码人员也能轻松构建企业级应用。通过可视化拖拽和模型驱动,实现快速开发和部署,加速企业信息化进程。利用道一云七巧低代码平台,企业可以快速实现个性化应用开发,规范流程管理,显著提升团队协作效率。作为高生产力aPaaS平台,道一云七巧是企业数字化转型的理想选择。
欢迎关注:
官网:道一云七巧 - 可视化、智能化、数字化应用构建
免费体验:道一云产品免费试用
公众号:道一云低代码(do1info)