浅谈前端水印

又是一个有关安全的问题。

通常状况下,咱们说的水印是指图片角落上的平台用户名水印。相似于下方图片上的这种,一般只要将图片上传到平台上,平台就会在图片上嵌入水印,固然,有些平台也会提供设置是否须要显示这种水印的开关,或者设置保存的时候才会加上水印。css

image.png

明水印

这种水印的实现实际上是比较简单的,就是将两张图片合成一张,或者是直接在原图上绘制内容就好了:html

<img id="pic" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3c3c98ebfce4ae28db981dfabedc1d8~tplv-k3u1fbpfcp-zoom-1.image" alt="原始图片" height="500" crossorigin="anonymous">
<div>Photo by Claudio Schwarz | @purzlbaum on Unsplash</div>
window.onload = () => {
    const pic = document.querySelector('#pic');
    const canvasNode = document.createElement('canvas');
    const picWithWatermark = createImageWithWatermark(pic, canvasNode);
    pic.src = picWithWatermark;
}


/**
 * 建立带水印的图片
 * create image with watermark.
 * @param {HTMLImageElement} img 图片结点 - image element.
 * @param {HTMLCanvasElement} canvas canvas结点 - canvas element.
 * @returns 处理后的图片 base64 - pic with watermark.
 */
const createImageWithWatermark = (img, canvas) => {
    const imgWidth = img.width;
    const imgHeight = img.height;
    canvas.width = imgWidth;
    canvas.height = imgHeight;

    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
    ctx.font = '16px YaHei';
    ctx.fillStyle = 'black';
    ctx.fillText('Photo by Claudio Schwarz | @purzlbaum on Unsplash', 20, 20);

    return canvas.toDataURL('image/jpg');
}

以上就是完整的代码了,更详细的代码能够访问github连接查看前端

普通用户所说的水印就是上面这种了,可是对于开发者来讲,水印所包含的分类仍是比较多的。node

如咱们在公司内网的部分系统(也多是全部)上就能看到这种水印。git

image.png

这里水印颜色选择黑色只是为了能更直观的看到效果,真实使用这种水印的时候,都会选用白色透明的。

这种水印就有点相似以前所说的,将两张图片合成一个的那种方式,只不过,在前端页面上,咱们是使用一个透明的canvas容器覆盖整个页面,而后在canvas中绘制这个“标识”,用来标识访问当前页面的用户身份,这样一来,不管是你截图仍是拍照,只要图片上能看到水印,咱们就能根据这个水印去追踪到泄露这部分信息的人。github

那可能会有人问,那我知道这个水印是一个dom结点了,打开控制台找到他,删了不就行了?算法

明水印的防护

这确实是好问题,不过也不是什么大的问题,你想删,这是彻底能够的。canvas

我控制不了你的行为,可是我能够检测到你操做了这个dom结点,那很差意思,我无论你怎么操做的这个结点,为了安全,我确定都要从新绘制这个水印的。后端

但光从新绘制水印我以为还不够,这可能会让你跟我拼速度的,那不行啊,我必须给你点教训的,还不能让你得偿所愿,怎么办?只要你操做了个人dom,那么我直接让页面白屏,而后再重载页面。这也就达成了禁止用户操做dom结点的方式了。安全

要实现这个,咱们须要借助js提供的MutationObserver函数,这个函数能够监听容器的变化。

代码以下:

// 容器监听的回调
const cb = function (mutationList, observer) {
    for (const mutation of mutationList) {
        if (mutation.type === 'childList') {
            const { removedNodes = [] } = mutation;
            // 若是监听到水印容器变化,那么就清空页面并重载
            const node = Array.prototype.find.apply(removedNodes, [(node => node.id === 'page-watermark')])
            if (node) {
                targetNode.innerHTML = '';
                window.location.reload();
            }
        }
    }
}
// 目标DOM结点
const targetNode = document.querySelector('#watermark-body');
// 建立监听
const observer = new MutationObserver(cb);
observer.observe(targetNode, {
    attributes: true,
    childList: true
});

MutationObserver是DOM3 Event规范的一部分,用于替代旧的Mutation Events,能够放心使用。

虽然上面的是全局水印,可是你也能够只对一部份内容加水印,只不过全局水印实现成本更低,代价小,对于内网系统来讲,牺牲这点用户体验,并不能算是什么很是严重的问题,是能够接受的。

可能有人又要说了,我都打开dom,那我研究一下这个dom结构,写个爬虫去爬数据,或者直接复制dom里面的内容不就行了,你这水印还有啥存在的意义吗?

没法反驳,可是要说明一点的是,爬数据这个是违法的,要负法律责任,并且你爬虫确定是要运行在某个电脑上的,这就不须要水印了,咱们能够直接查ip,追踪到对应的人就好了,而咱们加的水印不过就是一个方便追踪的工具而已。

其次,前端和爬虫斗智斗勇,你从网页爬数据,那我就想办法不直接生成文字,而是把一些关键词给替换成图片,这样一来,你爬虫爬到的结果,就是一串没有用的文字。

这就扯到反爬虫的事情上了。言归正传,到目前为止,咱们一直都在讨论明水印,对于内网来讲,使用这种水印确定是没什么问题的,可是对外的网站怎么办呢?若是也加上这种明水印,显然不太合适,想要在这里牺牲用户体验就是不能接受的。

因此咱们就开始考虑,能不能加上一个肉眼看不见的水印呢?

暗水印

固然是没问题的,这就是咱们下面要说的暗水印。

听名字就知道,暗水印和明水印是恰好相反的,咱们看不见这种水印,并且这种水印不管是原理仍是实现,和明水印的差异都是比较大的。

先看看原理。

不知道你有没有据说过,隐写术1。对于这个比较玄幻的名词,wiki是这么描述的“隐写术是一门关于信息隐藏的技巧与科学,所谓信息隐藏指的是不让除预期的接收者以外的任何人知晓信息的传递事件或者信息的内容。”,究其本质,仍是密码学那一套。

追加文件内容

咱们能够经过各类方式将信息写到图片,最多见的应该是将须要隐写的内容以二进制的形式写入图片中,我们在这里举个简单的例子,如下面的图片为例:

image.png

这是咱们开篇引用的图片,记为原始图像,将图片保存在本地后(original.png),执行命令:

tail -c 50 1.png

image.png

能够看到执行结果里面是一串乱码(用Hex查看器能够看到文件的二进制码流,这里是utf-8,乱码是正常的),对该文件执行命令:

cat original.png > result.png
echo testWrite >> result.png
tail -c 50 result.png

咱们生成一张新的图片以后,将一串字符追加到图片末尾,能够看到图片依旧是正常显示的,同时查看图片的内容,能够看到刚才写入的testWrite字符串:

image.png

另外,将字符串加到文件头部是不行的,由于文件头部包含了文件格式等信息。若是你把信息插入到文件头部,市面上的软件就没法正确的识别文件的类型。

固然了,你能够本身设计编码解码器来建立新的文件类型。

这只是一种方式,并且手段十分暴力,处理以后的图片文件较原来的文件是有必定的大小变化的(不过比较小,能够按字节计算)。更聪明的作法是将加密的信息按照某种模式写入图片的二进制流中,这样一来,就只有加密方才能拿到对应的信息了。

但即便有复杂的加密方式,也仍是不够的,由于这只能保证别人在使用原始图片的时候,咱们能够鉴别图片的来源、流传路线,但要是经过屏幕截图或者拍照的方式,咱们就没法拿到这个数据,由于此时相对于咱们作过处理的图片,他已是一张全新的图片了。

修改RGB份量值

来看另外一个例子,RGB份量值的小量变更:在图片上覆盖一层肉眼看不见的图片,简单来讲就是我能够在图片的某个单通道(如rgb中的b通道)内将水印信息写入,其实这么说也仍是很难懂,举个例子:

image.png

如今要将左右两侧的图片组合,可是不能让右侧的图片内容在左侧的图片上观察到,这时候咱们要作的就是按照必定规则将水印图片写进这张图片的rgb通道内。

预处理,先生成右侧的水印图

编码
1. 经过canvas获取到两张图片的rgba数据
2. 将左侧图片的b(蓝色)通道值-1,即,b & 0xfffffffe
3. 读取右侧b通道数据,遇到大于0的值,就将左侧对应位置处的b通道值 +1,即,b | 0x00000001

解码
1. 获取图片的rgba数据
2. 读取b通道数据,遇到 b & 0x00000001 > 0 的数据,说明有水印信息,将其置为255,除a通道(alpha通道不是颜色通道)外,其他通道的数据所有置为0


// +1,-1 是由于量级的变化极小,并不会影响到图片的显示
其实黑底蓝字的图片就是解码出来的水印数据,详细代码:

好像这种方式能够在用户截图时也可以保留咱们的水印?其实并无。

image.png

这是解码截图的结果,能够明显的看到,QQ截图以后的图片并无可以解码出来咱们所须要的水印内容,甚至于将图片压缩以后,可能就会失去咱们的水印,因此说这其实也并非一个可靠的水印方式。

那如何才能保证咱们的水印至少在截图的时候也能发挥做用呢?

也不是不行,首先肯定咱们水印要加在哪里(肯定需求),由于图片来源无非是网页搜索结果,或者说咱们截得图多数来自于网页,因此咱们考虑的是在网页上覆盖一层水印,保证用户从网页上截取的图片能够被咱们追踪到来源。

这个通用的解决方案依旧是写css,只不过这时候咱们将背景图置顶,同时将其透明度设置的很低。

代码很简单,其实就是将一张背景图片铺满整屏就能够了,而后将opacity设置到肉眼没法观察到的程度就OK了:

window.onload = () => {
    const width = document.body.clientWidth;
    const height = document.body.clientHeight;

    const maskDiv = document.createElement('div');
    maskDiv.id = 'mask_watermark';
    maskDiv.style.position = 'absolute';
    maskDiv.style.backgroundImage = 'url(./1.jpg)';
    maskDiv.style.backgroundRepeat = 'repeat';
    maskDiv.style.visibility = '';
    maskDiv.style.left = '0px';
    maskDiv.style.top = '0px';
    maskDiv.style.overflow = "hidden";
    maskDiv.style.zIndex = "9999";
    maskDiv.style.pointerEvents = "none";
    maskDiv.style.opacity = 0.005;
    maskDiv.style.fontSize = '20px';
    maskDiv.style.color = '#000';
    maskDiv.style.textAlign = "center";
    maskDiv.style.width = `${width}px`;
    maskDiv.style.height = `${height}px`;
    maskDiv.style.display = "block";
    document.body.appendChild(maskDiv);
}

image.png

左侧是从网页上接下来的图片,右侧是在PS工具中处理以后的图片2,明显能够看到咱们设置的水印。

而生成图片的方式就有不少种了,能够是前端生成,也能够是将信息发给后端,后端生成一张图片,而后前端将图片做为背景图。

想要获得右侧的结果,未必须要PS进行处理,能够经过其余的方式进行处理。

到这里,前端部分就结束了,但可能有人还以为这不太行,我截网页的图如今是加上了水印,可是我要是保存原图呢?那能够用以前说的RGB份量那个方式。

那我下载图片以后在原图上截取呢,不就失效了?确实,到这里前端能作的工做已经不多了。咱们已经处理不到了,可是在图像暗水印,或者说盲水印这个领域,还有更加有效的抵抗攻击(去水印)的方式,好比频域、空域的变换。这个变换能够说是老生常谈的了,我就不过多解释了。

补充两句

水印的概念是泛化的,并非说只有显示在图片某个角落的信息才能被称为水印。

上面选择将信息追加到文件末尾是有缘由的,不是瞎选的。任何一种文件都包含文件结束符,就如文件头部约定存放文件的格式信息同样,即便你改了后缀,我也能经过读取这个文件头部的内容来识别文件真实的格式。

另外咱们知道,文件后缀名是能够随意更改的,若是只经过文件后缀名进行检测,那么绝对是能够绕过的,进而出现任意文件上传的安全问题。

若是改变图层混合模式没能成功,不妨试下修改图像的RGB曲线

参考文章

相关文章
相关标签/搜索