乐趣区

关于javascript:浅谈前端水印

又是一个无关平安的问题。

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

明水印

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

<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 链接查看。

普通用户所说的水印就是下面这种了,然而对于开发者来说,水印所蕴含的分类还是比拟多的。

如咱们在公司内网的局部零碎(也可能是所有)上就能看到这种水印。

这里水印色彩抉择彩色只是为了能更直观的看到成果,实在应用这种水印的时候,都会选用红色通明的。

这种水印就有点相似之前所说的,将两张图片合成一个的那种形式,只不过,在前端页面上,咱们是应用一个通明的 canvas 容器笼罩整个页面,而后在 canvas 中绘制这个“标识”,用来标识拜访以后页面的用户身份,这样一来,无论是你截图还是拍照,只有图片上能看到水印,咱们就能依据这个水印去追踪到泄露这部分信息的人。

那可能会有人问,那我晓得这个水印是一个 dom 结点了,关上控制台找到他,删了不就好了?

明水印的进攻

这的确是好问题,不过也不是什么大的问题,你想删,这是齐全能够的。

我控制不了你的行为,然而我能够检测到你操作了这个 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 是这么形容的“隐写术是一门对于信息暗藏的技巧与迷信,所谓信息暗藏指的是不让除预期的接收者之外的任何人通晓信息的传递事件或者信息的内容。”,究其实质,还是密码学那一套。

追加文件内容

咱们能够通过各种形式将信息写到图片,最常见的应该是将须要隐写的内容以二进制的模式写入图片中,咱们在这里举个简略的例子,以上面的图片为例:

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

tail -c 50 1.png

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

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

咱们生成一张新的图片之后,将一串字符追加到图片开端,能够看到图片仍旧是失常显示的,同时查看图片的内容,能够看到方才写入的 testWrite 字符串:

另外,将字符串加到文件头部是不行的,因为文件头部蕴含了文件格式等信息。如果你把信息插入到文件头部,市面上的软件就无奈正确的辨认文件的类型。

当然了,你能够本人设计编码解码器来创立新的文件类型。

这只是一种形式,而且伎俩非常暴力,解决之后的图片文件较原来的文件是有肯定的大小变动的(不过比拟小,能够按字节计算)。更聪慧的做法是将加密的信息依照某种模式写入图片的二进制流中,这样一来,就只有加密刚才能拿到对应的信息了。

但即便有简单的加密形式,也还是不够的,因为这只能保障他人在应用原始图片的时候,咱们能够甄别图片的起源、流传路线,但要是通过屏幕截图或者拍照的形式,咱们就无奈拿到这个数据,因为此时绝对于咱们做过解决的图片,他曾经是一张全新的图片了。

批改 RGB 重量值

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

当初要将左右两侧的图片组合,然而不能让右侧的图片内容在左侧的图片上察看到,这时候咱们要做的就是依照肯定规定将水印图片写进这张图片的 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 是因为量级的变动极小,并不会影响到图片的显示 

其实黑底蓝字的图片就是解码进去的水印数据,具体代码:

如同这种形式能够在用户截图时也可能保留咱们的水印?其实并没有。

这是解码截图的后果,能够显著的看到,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);
}

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

而生成图片的形式就有很多种了,能够是前端生成,也能够是将信息发给后端,后端生成一张图片,而后前端将图片作为背景图。

想要失去右侧的后果,未必须要 PS 进行解决,能够通过其余的形式进行解决。

到这里,前端局部就完结了,但可能有人还感觉这不太行,我截网页的图当初是加上了水印,然而我要是保留原图呢?那能够用之前说的 RGB 重量那个形式。

那我下载图片之后在原图上截取呢,不就生效了?的确,到这里前端能做的工作曾经很少了。咱们曾经解决不到了,然而在图像暗水印,或者说盲水印这个畛域,还有更加无效的抵制攻打(去水印)的形式,比方频域、空域的变换。这个变换能够说是陈词滥调的了,我就不过多解释了。

补充两句

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

下面抉择将信息追加到文件开端是有起因的,不是瞎选的。任何一种文件都蕴含文件结束符,就如文件头部约定寄存文件的格局信息一样,即便你改了后缀,我也能通过读取这个文件头部的内容来辨认文件实在的格局。

另外咱们晓得,文件后缀名是能够随便更改的,如果只通过文件后缀名进行检测,那么相对是能够绕过的,进而呈现任意文件上传的平安问题。

如果扭转图层混合模式没能胜利,无妨试下批改图像的 RGB 曲线

参考文章


  1. 不能说的机密——前端也能玩的图片隐写术 | AlloyTeam ↩
  2. 阿里巴巴内网的不可见水印用的是什么算法?– Mize 的答复 – 知乎 ↩
退出移动版