浅析Web前端水印方案:前端加水印和服务端加水印的适用场景、不同的实现方案、使用 MutationObserver 监听dom元素变化、MutationObserver API 介绍

一、问题背景

  为了防止信息泄露或知识产权被侵犯,在web的世界里,对于页面和图片等增加水印处理是十分有必要的,水印的添加根据环境可以分为两大类,前端浏览器环境添加和后端服务环境添加,根据实现方式又可以分为两大类,显性水印数字水印。简单对比一下这两种方式的特点:

1、前端浏览器加水印:

(1)减轻服务端的压力,快速反应

(2)安全系数较低,对于掌握一定前端知识的人来说可以通过各种骚操作跳过水印获取到源文件

(3)适用场景:资源不跟某一个单独的用户绑定,而是一份资源,多个用户查看,需要在每一个用户查看的时候添加用户特有的水印,多用于某些机密文档或者展示机密信息的页面,水印的目的在于文档外流的时候可以追究到责任人

2、后端服务器加水印:

(1)当遇到大文件密集水印,或是复杂水印,占用服务器内存、运算量,请求时间过长

(2)安全性高,无法获取到加水印前的源文件

(3)适用场景:资源为某个用户独有,一份原始资源只需要做一次处理,将其存储之后就无需再次处理,水印的目的在于标示资源的归属人。

3、显性水印:容易处理,算法较为简单;可以通过裁剪、模糊等操作对水印进行攻击消除,同时显性水印也会破坏图片的完整性。

4、数字水印:算法一般较为复杂,抗攻击能力较强。

  当然,优缺点也需要分情况来看,各个方案都拥有自己的优缺点,需要使用者在安全性、性能之间衡量。没有最好的方案,只有根据环境与需求,使用当前最适合的方案。

二、调研 - 看看其他网站如何处理的

  比如:CSDN、知乎、微博,都是直接 img 显示 url,当获取到 url 在浏览器下打开的时候,获取到的是已经添加水印的图片。(也有可能水印应该不止所见)

  • 后端直接处理增加水印(前端直接使用img标签显示url
  • 暂时只能看到表层右下角水印(用户名)
  • 是否添加数字水印(未知)

  从这三个网站的特点来看,这种做法是很合适的,因为他们需要添加水印的资源往往都绑定到了某一个用户上,也就是,一份原始资源只需要做一次处理(前后端都可),将其存储之后就无需再次处理。

  但是,这种方式在某些情况下是却不是最佳的。比如资源不跟某一个单独的用户绑定,而是一份资源,多个用户查看,需要在每一个用户查看的时候添加用户特有的水印。这多用于某些机密文档或者内部文件,水印的目的在于文档外流的时候可以追究到责任人。故而,添加水印的方法需要根据环境的不同需求的不同来变化。

三、实现方案

1、显性水印 + DOM元素直接遮盖 - 重复的 dom 元素覆盖实现

  从效果开始,要实现的效果是「在页面上充满透明度较低的重复的代表身份的信息」,第一时间想到的方案是在页面上覆盖一个 position:fixed 的div盒子,盒子透明度设置较低,设置  pointer-events: none;样式实现点击穿透,在这个盒子内通过 js 循环生成小的水印div,每个水印div内展示一个要显示的水印内容。

  页面效果是有了,但是这种方案需要要在js内循环创建多个dom元素,既不优雅也影响性能,于是考虑可不可以不生成这么多个元素。

  将水印文字直接通过一层DOM元素,覆盖到需要添加水印的图片上,并且可以添加两层,一层为明显水印,其透明度较高,肉眼可见,一层为隐藏水印透明度极低,肉眼无法分辨,但可以通过一些处理后续显现出来(以PS为例:现在把图片放到PS里面,建一个图层在上面,全部填充为黑色,混合模式选择颜色加深这一类的(也就是让亮的更亮,暗的更暗))

  这样,在用户截图的之后,就算涂抹掉了明显水印,可由于隐藏水印肉眼无法分辨,简单的涂抹攻击并不能准确定位到隐藏水印。

  当对于图片完整性要求不高(也就是铺满了水印都不介意,只要看清内容即可)的情况,建议增加水印密度,直到只要用户去涂抹水印,就会直接破坏文件到无法阅读的地步

2、显性水印+Canvas:canvas 输出背景图,或 svg 实现背景图

  第一步还是在页面上覆盖一个固定定位的盒子,然后创建一个canvas画布,绘制出一个水印区域,将这个水印通过toDataURL方法输出为一个图片,将这个图片设置为盒子的背景图,通过backgroud-repeat:repeat;样式实现填满整个屏幕的效果。

  与canvas生成背景图的方法类似,只不过是生成背景图的方法换成了通过svg生成,canvas的兼容性略好于svg。

  其实现和显性水印+DOM元素直接遮盖一样,但其性能优于方案一,直接通过Canvas绘画,避免了在水印密度较大的情况下大量DOM元素的创建与添加,并且Canvas在部分环境与浏览器下拥用GPU加速的功能,故而性能提升较大。

  具体一些实现代码参考,可以看这篇文章:https://mp.weixin.qq.com/s/7NxQMtolD3UL5qDBsDkIWw

四、如何防止删除 dom 元素去除水印

  这样看起来能满足我们的需求了,但是还有一个问题,稍微懂一点浏览器的使用或网页知识的用户,可以用浏览器的开发者工具来动态更改DOM的属性或者结构就可以去掉了。我们可以使用 MutationObserver 来监听 dom 元素变化MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时做出适当反应的能力。

  MutationObserver兼容性可以看出高级浏览器以及移动浏览器支持非常不错。突变观察员 API 用来监视DOM变动。DOM的任何变动,比如节点的增减,属性的变动,文本内容的变动,这个 API 都可以得到通知。

  使用MutationObserver的实例的观察函数方法用来启动监听,它接受两个参数:第一个参数:所要观察的DOM节点,第二个参数:一个配置对象,指定所要观察的特定变动,有以下几种:

属性 描述
childList 如果需要观察目标节点的子节点(新增了某个子节点,或者移除了某个子节点),则设置为true.
attributes 如果需要观察目标节点的属性节点(新增或删除了某个属性,以及某个属性的属性值发生了变化),则设置为true.
characterData 如果目标节点为characterData节点(一种抽象接口,具体可以为文本节点,注释节点,以及处理指令节点)时,也要观察该节点的文本内容是否发生变化,则设置为true.
subtree 除了目标节点,如果还需要观察目标节点的所有后代节点(观察目标节点所包含的整棵DOM树上的上述三种节点变化),则设置为true.
attributeOldValue 在attributes属性已经设为true的前提下,如果需要将发生变化的属性节点之前的属性值记录下来(记录到下面MutationRecord对象的oldValue属性中),则设置为true.
characterDataOldValue 在characterData属性已经设为true的前提下,如果需要将发生变化的characterData节点之前的文本内容记录下来(记录到下面MutationRecord对象的oldValue属性中),则设置为true.
attributeFilter 一个属性名数组(不需要指定命名空间),只有该数组中包含的属性名发生变化时才会被观察到,其他名称的属性发生变化后会被忽略.

  MutationObserver只能监测到某种属性改变,增减子结点等,对于自己本身被删除,是没有办法的,可以通过监测父结点来达到要求。检测代码实现如下:

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
  let mo = new MutationObserver(function () {
    const __wm = document.querySelector('.__wm');
    // 只在__wm元素变动才重新调用 __canvasWM
    if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
      // 避免一直触发
      mo.disconnect();
      mo = null;
      __canvasWM(JSON.parse(JSON.stringify(args)));
    }
  });
  mo.observe(container, {
    attributes: true,
    subtree: true,
    childList: true
  })
}

  Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

  概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发

  这样设计是为了应付 DOM 变动频繁的特点。举例来说,如果文档中连续插入1000个<p>元素,就会连续触发1000个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿;而 Mutation Observer 完全不同,只在1000个段落都插入结束后才会触发,而且只触发一次。

  Mutation Observer 有以下特点:

(1)它等待所有脚本任务完成后,才会运行(即异步触发方式)。

(2)它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。

(3)它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。

  关于 Mutation Observer API 的详细介绍及具体如何使用,可以看这篇文章:《Mutation Observer API》- http://javascript.ruanyifeng.com/dom/mutationobserver.html

posted @ 2021-09-30 21:19  古兰精  阅读(2120)  评论(0编辑  收藏  举报