网页水印技术初探

共 7434字,需浏览 15分钟

 ·

2022-06-13 02:59

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

前言

为了防止信息泄露或知识产权被侵犯(版权视频搬运侵权取证、防篡改、链路追踪),我们经常能在网页上看到水印的存在,水印能够很好的保护知识产权,因此我们在公司的内部资料,或者是购买的一些数字资料上往往都能看到数字水印的身影存在。

水印的分类

从添加方式上来讲,水印可以分为前端浏览器环境添加和后端服务环境添加,他们的区别如下:

前端浏览器加水印

  • 方便简单,快速反应,减轻服务端的压力
  • 安全系数较低,对于掌握一定前端知识的人来说可以通过各种骚操作跳过水印获取到源文件
  • 适用场景:资源不跟某一个单独的用户绑定,而是一份资源,多个用户查看,需要在每一个用户查看的时候添加用户特有的水印,多用于某些机密文档或者展示机密信息的页面,水印的目的在于文档外流的时候可以追究到责任人

后端服务器加水印

  • 安全性高,无法获取到加水印前的源文件
  • 当遇到大文件密集水印,或是复杂水印,占用服务器内存、运算量,请求时间过长
  • 适用场景:资源为某个用户独有,一份原始资源只需要做一次处理,将其存储之后就无需再次处理,水印的目的在于标示资源的归属人

从水印的展现形式上来讲,水印又可以分为 明水印暗水印

接下来我们主要讲解明水印和暗水印的实现方式。

明水印

明水印指肉眼看得到的水印,就是将水印内容的元素覆盖在目标元素上面。我们常见的明水印如下

明水印的实现技术

基本前提:在全屏水印的场景下,我们通常给水印设置为最高z-index的绝对定位元素,并用pointer-events: none实现点击穿透,不影响用户的正常使用。

目前明水印方案主要有以下几种:

DOM

1.生成窗口尺寸一样的父级DOM,通过绝对定位盖在内容上

2.通过拿到的水印信息生成每一个独立的DOM

3.通过 css布局将dom排列到父级上

Canvas

1.利用canvas 绘制一个水印

2.通过canvas.toDataURL() 来拿到文件流的url

3.将url填充在一个元素的背景中,设置背景图片的属性为repeat

Shadow DOM

基本实现思路和上面DOM类似,只是此处不直接生成DOM,而是使用shadow dom,相比上面的直接使用DOM,其优点在于与页面上其他的的DOM树隔离,性能上更好,同时不会影响到页面本有的DOM功能。

如何防止明水印被清除

明水印相对有计算机知识的同学来说还是很容易破解的,因为原理都是在图片/ DOM上层添加一层遮罩实现的,只要找到对应DOM  Wrapper删除遮罩的DOM即可。

为了避免出现这种事,前端还是可以做出点努力的,这就用到了Mutation Observer API,MutationObserver是元素观察器,用来观节点变化的。

比如下面这个 demo 就是监听水印 DOM 被删除的话就重新生成一个新的。

代码如下所示

      function createMask({
        let canvas = document.createElement("canvas");
        canvas.id = "__canvas";
        canvas.width = "120"// 每个水印的宽高
        canvas.height = "70";
        let ctx = canvas.getContext("2d");
        ctx.fillStyle = "rgba(200, 180, 150, 0.5)";
        ctx.rotate((45 * Math.PI) / 180); // 偏转的角度
        ctx.fillText("hockor-test"3020); // 绘制文本 绘制开始位置

        let src = canvas.toDataURL("image/png");

        // 水印容器
        let waterMaskDiv = document.createElement("div");

        waterMaskDiv.style.position = "fixed";
        waterMaskDiv.style.zIndex = "-1";
        waterMaskDiv.id = "water-mark";
        waterMaskDiv.style.top = "0";
        waterMaskDiv.style.left = "0";
        waterMaskDiv.style.height = "100%";
        waterMaskDiv.style.width = "100%";
        waterMaskDiv.style.pointerEvents = "none";
        waterMaskDiv.style.backgroundImage = "URL(" + src + ")";
        // 水印节点插到body下 可以通过层级控制节点层次
        document.body.appendChild(waterMaskDiv);
      }

      createMask();

      let config = {
        childListtrue,
        attributestrue,
        characterDatatrue,
        subtreetrue,
        attributeOldValuetrue,
        characterDataOldValuetrue,
      };

      const mutationCallback = (mutationList) => {
        for (let mutation of mutationList) {
          mutation.removedNodes.forEach(function (item{
            if (item.id === "water-mark") {
              createMask();
            }
          });
        }
      };

      // 创建 MutationObserver 实例
      let observer = new MutationObserver(mutationCallback);

      // 开始监控目标节点
      observer.observe(document.body, config);

      // 停止监控
      // observer.disconnect()

总结:




方案优点缺点
DOM1、实现简单1:水印节点太多会影响页面性能
2:容易篡改
Canvas1、实现逻辑比较清晰
2、水印数据生成图片,用户想篡改比较难
1:浏览器版本有要求
Shadow dom1、性能好
2、与原本DOM隔离
1:不能被Mutation Observer监听
2:容易被篡改

暗水印

暗水印指肉眼看不到的水印。暗水印可以理解为,在一些载体数据中添加隐藏标记,这些标记在人类和机器可轻易感知的范围之外。相较于常见的明水印,比如图片和视频中的公司logo、纸币中的水印纹理等。暗水印对大部分感知系统来说是透明的,不可见的。

古老的暗水印

比如下图是实际存在的隐写耐火纸,可以看到在一张看似普通的白纸之中,却隐藏了一个图案和字母。这个图案和字母就属于暗水印。它可以用来隐秘传输信息、做防伪标识等。

基于图像的暗水印技术是暗水印里面最成熟的一种,嵌入方法也多种多样。根据嵌入维度不同,又可以细分为空域水印和变换域水印。空域水印可以简单的理解为直接对解码后的图像像素值进行编辑和嵌入信息;变换域水印是将图像的像素信息转换到变换域,然后在变换域添加信息后再转换到空域,这个过程中空域信息也会被修改。所以变换域水印也可以理解为间接的空域水印。

空域水印(LSB算法)

对于图片资源来说,显性水印会破坏图片的完整性,有些情况下我们想要在保留图片原本样式,这时可以添加隐藏水印。比如我们可以在页面的背景图上面插入暗水印。

实现思路是:图片的像素信息里存储着 RGB 的色值,对于RGB 分量值的小量变动,是肉眼无法分辨的,不会影响对图片的识别,我们可以对图片的RGB以一种特殊规则进行小量的改动。

这种算法也叫LSB隐写(最低有效位的隐写),这种隐写方式需要图片是无压缩的位图,因此一般用于bmp和png图片。

看个例子

此算法计算复杂度相对较低;对图像视觉效果影响很小;但是其鲁棒性较低,难以抵抗常见的水印攻击手段(裁剪、拉伸等等)。

变换域水印

变换域水印最终也会修改空域的数据,与上面不同的是并不是直接修改像素值,而是将图像的空域数据转换到变换域,然后按照一定方法写入水印信息,最后再将变换域数据转换回空域的值并重新生成图像信息。

简单复习下傅里叶变换

傅里叶变换简单地说就是将信号在时域空域的函数转变到频域表示,在和工程学中有许多应用。因其基本思想首先由法国学者约瑟夫·傅里叶系统地提出。

先简单理解下时域和频域

如图所示,我们可以把任何波形图看成 N 个普通正弦/余弦函数的叠加,我们可以通过控制其中的部分函数来达到改变信息的效果。

那么,傅里叶变换有什么用呢,

  • 先在纸上画一个sin(x),不一定标准,意思差不多就行。不是很难吧。
  • 好,接下去画一个sin(3x)+sin(5x)的图形。这个就很难能画得出来。

从时域的角度来讲:给你一个sin(3x)+sin(5x)的曲线,只看图是看不出这整个曲线的方程式是怎样的,现在需要将把sin(5x)从图里拿出去,看看剩下的是什么,这更是不可能做到的。

但是从频域来看呢?则简单的很,无非就是几条竖线而已。我们将其从频域中抽掉即可。

所以很多在时域看似不可能做到的数学操作,在频域相反很容易。这就是需要傅里叶变换的地方。尤其是从某条曲线中去除一些特定的频率成分,这在工程上称为滤波,是信号处理最重要的概念之一,只有在频域才能轻松的做到。

我们常说一个音有多高,这个音高是指频率;同样,图像灰度变化强烈的情况,也可以视为图像的频率。

频域添加数字水印的方法,是指通过某种变换手段(傅里叶变换,离散余弦变换,小波变换等)将图像变换到频域(小波域),在频域对图像添加水印,再通过逆变换,将图像转换为空间域。相对于空域手段,频域手段隐匿性更强,抗攻击性更高

这里有一篇很好的讲傅里叶变换的文章,推荐给大家:

https://zhuanlan.zhihu.com/p/19763358

暗水印的特性

最后我们来总结下暗水印的特点:

  • 具有很强的隐蔽性
  • 具有不可以移除性
  • 具有很强的鲁棒性

总结

最后我们对明水印和暗水印来做一个总结:

  • 明水印,实现简单,看得见摸得着,一方面能直接溯源找到是谁泄露的,另一方面也起到警示作用,让泄露者不敢轻易外泄,但防篡改和防删除能力极低。

  • 暗水印,实现复杂,看不见摸不着,其用肉眼观察几乎没有任何分别,但是一旦发生数据泄露,通过密钥和水印提取算法就可以解出添加的水印,直接定位到泄露点和人。

参考文档:

  • https://www.secrss.com/articles/32116
  • https://ulyc.github.io/2019/03/15/盲水印和图片隐写术/
  • https://www.zhihu.com/question/50735753
Node 社群



我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。



如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章
2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持

浏览 158
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报