前言
水印的目的是为了防止信息泄漏,保护版权,在很多网站里都有用到了水印,水印真的只是看到的这么简单吗?
接触到水印相关知识是因为一个需求,也是因为这个需求给我打开了水印相关的大门。当时有个需求是给一个图片添加任意多个水印,并且这些水印可以让用户拖拽到图片的任意位置,做完这个功能之后为了也把它抽出来做成了一个npm插件:add-move-water-picture,不过这个插件是当时随手写的,一直都没有在完善了,准备有时间重新整理一下。
水印的实现
言归正传,水印的添加可以分为前端环境添加和后端环境添加,这两者各有各的好处,网上有很多这里就不一一列举了,这不是文章的重点。
当你在浏览这篇文章时,屏幕上就有你的英文名,这个就是明水印了,稍微懂一些开发的人员就可以让这个水印消失,各位同学可以找一个带有水印的网页,然后大家控制台进入Elements中直接搜索watermark大概率都会搜到一个class为watermark的dom元素,这个就是水印
。点击水印可以看到它添加水印的方式,通常都是使用背景图的方式添加。
下面就来一一实现这些水印到添加方式。
全屏覆盖水印
既然想让屏幕出现水印,第一个想到的就是用一个东西遮在网页内容上层不就可以了吗,这样每次截图、拍照都会把上层内容包裹进去。这里的实现方式就是利用绝对定位让一个div在内容上层,这个div的宽高和内容区一样,然后生成一个个固定宽高的小div,求出多少列和多少行生成对应的元素即可。
<style>
.waterWrapper {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-wrap: wrap;
pointer-events: none;
overflow: hidden;
box-sizing: border-box;
}
</style>
<div class="water">
我是网页内容
<p>我是网页内容</p>
<p>我是网页内容</p>
<p>我是网页内容</p>
</div>
<script>
// div和绝对定位形式
function cssHelper(el, prototype) {
for (let i in prototype) {
el.style[i] = prototype[i]
}
}
function createItem() {
const item = document.createElement('div')
item.innerHTML = '这是水印'
cssHelper(item, {
position: 'absolute',
top: `50px`,
left: `50px`,
fontSize: `16px`,
color: '#000',
lineHeight: 1.5,
opacity: 0.1,
transform: `rotate(-15deg)`,
transformOrigin: '0 0',
userSelect: 'none', // 用户无法选中
whiteSpace: 'nowrap',
})
return item;
}
function createWater() {
const waterHeight = 100;
const waterWidth = 160;
const { clientWidth, clientHeight } = document.documentElement ||
document.body;
// 不能使用ceil向上取整,否则会出现超出一屏的水印
const column = Math.floor(clientWidth / waterWidth);
const rows = Math.floor(clientHeight / waterHeight);
const waterWrapper = document.createElement('div');
waterWrapper.className = 'waterWrapper'
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div');
cssHelper(wrap, Object.create({
position: 'relative',
width: `${waterWidth}px`,
height: `${waterHeight}px`,
flex: `0 0 ${waterWidth}px`,
overflow: 'hidden',
}));
wrap.appendChild(createItem());
waterWrapper.appendChild(wrap)
}
document.body.appendChild(waterWrapper)
}
createWater();
window.onresize = function() {
const wrapper =
document.getElementsByClassName('waterWrapper')[0];
document.body.removeChild(wrapper);
createWater();
}
效果如下:
canvas背景图
使用canvas也可以实现水印的添加,这个是利用背景图的方式配置repeat属性来铺满全屏。使用canvas生成一个小的图片,然后导出base64格式的图片设置为背景即可,具体可看代码:
<style>
.watermark {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
user-select: none;
pointer-events: none;
background-repeat: repeat;
}
</style>
</head>
<body>
<div>
这是网页内容
</div>
<script>
function createWater(text) {
const angle = -20;
const canvas = document.createElement('canvas');
canvas.width = 120;
canvas.height = 50;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 120, 50);
ctx.fillStyle = '#404040';
ctx.globalAlpha = 0.1;
ctx.font = `16px Avenir, Helvetica, Arial, sans-serif`
ctx.rotate(Math.PI / 180 * angle);
ctx.fillText(text, 0, 50);
return canvas.toDataURL();
}
const wrapper = document.createElement('div');
wrapper.className = 'watermark';
wrapper.style.backgroundImage = `url(${createWater('这是水印')})`;
document.body.appendChild(wrapper);
</script>
</body>
效果如下:
水印防篡改
像这种明水印可以说是防君子不防小人,因为可以很轻易的就隐藏掉这些水印,所以我们需要给水印做一些保护措施。我们知道想删除这个水印可以选中这个dom元素直接delete,也可以给属性设置display:none
隐藏掉。针对这些操作可以监听dom元素的变动。正好MutationObserver
可以满足这些条件,在进行dom的删除和修改属性时进行判断修改的是不是水印元素,如果是水印元素就立即生成一个新的添加或者替换进去,保证水印一直存在:
const watermark = document.getElementsByClassName('watermark')[0];
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for (let mutation of mutationsList) {
if (mutation.type === 'attributes') { // 监听属性
const target = mutation.target;
if (target === watermark) {
document.body.replaceChild(watermark, target);
}
}
mutation.removedNodes.forEach(function (item) { // 监听节点
if (item === watermark) {
document.body.appendChild(watermark);
}
});
}
};
// 监听元素
const targetNode = document.body;
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
这样就万无一失了吗?这当然不可能,浏览器是很强大的,会技术的程序猿更强大,可以利用一切可利用的工具来达到想要的目的。这些防篡改的方法都是基于js的,那我把浏览器的js禁了不就可以了吗,这样还是可以成功的把水印去掉。还有其他方式就不一一列举了。这种添加防篡改的方式只能过滤一些小白,有没有更加安全的水印添加方式呢?当然有了,那就是隐水印,加了水印却看不到,需要通过特定的方式解密后才可以看到。
图片隐水印
隐水印的实现可以利用颜色来实现,颜色可以用rgb或者rgba表示
上面一个div的背景色是rgb(233, 122, 55),下面的div的背景色是rgb(233, 122, 54),只是少了一个像素值,肉眼是看不出来的,可以利用这个原理来对图片做处理。
实现思路
我们拿图片为例,一个图片是由很多的数据组成,文字也是如此,可以通过canvas将图片绘制出来,然后通过getImageData函数拿到这个图片的数据信息,这个数据是一个数字,没有信息的数据是0,通过一定的规律来对图片的颜色修改进行加密,当需要显示出来的时候,再通过相同的规律进行解密。这就是大致的实现思路,图片的数据:
实现加密:
在写代码之前先定下一个加密规律:我们修改R通道的数据,将有信息存在的数据偶数变为奇数,解密的时候只填充偶数位,其余的全部不显示或者变为纯色即可。
const canvas = document.getElementById('canv')
const ctx = canvas.getContext('2d')
const img = new Image()
img.src = './article.jpeg'
let originData;
img.onload = function() {
ctx.drawImage(img, 0, 0)
originData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
mergeData(textData, 'R')
}
首先写一个canvas标签,也可以通过document.createElement('canvas')
来创建,给标签设置一个id位canv,再创建一个img对象,src设置为想加水印的图片,在img.onload方法中用canvas绘制出图片并获取到他的数据信息,其中的mergeData
就是加密方法。
let textData;
ctx.font = '15px Microsoft Yahei';
ctx.fillStyle = 'red'
ctx.fillText('bruce好帅我好爱', 60, 130);
textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
上面是添加水印文字,也是通过canvas来生成同时获取到他的data数据。实际上就是对两组像素数据信息进行处理。接下来就是加密的方法
// 加密过程
function mergeData(newData, color) {
// 获取图片的data
var oData = originData.data
console.log(newData, originData)
// bit是要改的通道所在的位置, offset是alpha相对于通道所在的位置
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;
}
// r g b a
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)) {
// 有信息的像素
oData[i]++
}
}
}
ctx.putImageData(originData, 0, 0)
}
只需要在图片的onload方法中调用这个加密函数即可。现在我们就来对一张图片进行加密
看着和原图是一样的,但是其实已经被加密了,上面已经被打入了文字。
实现解密
解密就比较简单了,只需要知道加密的方法然后对应的解出就可以,这里是对R通道进行的处理,所以我们这里判断R通道是否为奇数即可,是奇数就填充,不是奇数就全部置为0,实现代码:
// 解密函数
function processData(originData) {
var data = originData.data
for (var i = 0; i < data.length; i++) {
if (i % 4 === 0) {
// 像素通道
if (data[i] % 2 == 0) {
data[i] = 0
} else {
data[i] = 255
}
}
}
ctx.putImageData(originData, 0, 0)
}
解密的时候调用这个函数就可以,把页面上的图片右击保存,现在我们来看一下效果:
这就是解密后的样子,如果我们不想要原图的背景,可以在解密的循环里将其他通道都设为纯色,比如设置为0就是黑色背景:
以上就是暗水印的简单示例。这样加密也不是百分百安全,可以通过ps或者裁剪等一些其他方式把水印破坏,但是已经可以起到一定的安全保障。还有一种更好的添加隐水印的方法就是利用傅里叶变换来进行添加更加精准的水印,安全性会更高,感兴趣的同学可以阅读学习一下:傅里叶变换。