一文理解前端水印

  • Post author:
  • Post category:其他


明水印


明水印

(watermark)是一种容易识别、被夹于纸内,能够透过光线穿过从而显现出各种不同阴影的技术。水印的类型有很多,有一些是整个覆盖在图层上的水印,还有一些是在角落。

可以通过绝对定位,来将水印覆盖到我们的页面之上。不过,直接覆盖上去是无法触发底下图层的事件的,要使用到 css 属性

pointer-events。



pointer-events


属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的target。当它的被设置为

none

的时候,能让元素实体虚化,虽然存在这个元素,但是该元素不会触发鼠标事件。

明水印的生成方式主要可以归为两类,一种是

纯 html 元素(纯div)

,另一种则为

背景图(canvas/svg)

div 实现


user-select

属性控制用户能否选中文本。除了文本框内,它对被载入为 chrome 的内容没有影响。

首先生成一个水印块,包括设置透明度和旋转角度,以及使用

userSelect

属性,让此时的文字无法被选中:

function cssHelper(el, prototype) {
  for (let i in prototype) {
    el.style[i] = prototype[i];
  }
}
const item = document.createElement('div');
item.innerHTML = 'edemao';
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',
  overflow: 'hidden',
})

然后通过计算屏幕的宽高,以及水印的大小来计算需要生成的水印个数:

const waterHeight = 100;
const waterWidth = 180;
const { clientWidth, clientHeight } = document.documentElement || document.body;
const column = Math.ceil(clientWidth / waterWidth);
const rows = Math.ceil(clientHeight / waterHeight);
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);

背景图实现

canvas

主要是利用

canvas

绘制一个水印,将它转化为 base64 的图片,然后通过

canvas.toDataURL()

来拿到文件流的 url,将获取的 url 填充在一个元素的背景中,最后设置背景图片的属性为重复。

.watermark {
    position: fixed;
    top: 0px;
    right: 0px;
    bottom: 0px;
    left: 0px;
    pointer-events: none;
    background-repeat: repeat;
}
function createWaterMark() {
  const angle = -20;
  const txt = 'edemao';
  const canvas = document.createElement('canvas');
  canvas.width = 180;
  canvas.height = 100;
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, 180, 100);
  ctx.fillStyle = '#000';
  ctx.globalAlpha = 0.1;
  ctx.font = `16px serif`
  ctx.rotate(Math.PI / 180 * angle);
  ctx.fillText(txt, 0, 50);
  return canvas.toDataURL();
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`;
document.body.appendChild(watermakr);

svg

和 canvas 类似,主要还是生成背景图片:

function createWaterMark() {
  const svgStr =
    `<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
      <text x="0px" y="30px" dy="16px"
      text-anchor="start"
      stroke="#000"
      stroke-opacity="0.1"
      fill="none"
      transform="rotate(-20)"
      font-weight="100"
      font-size="16"
      >
      	edemao
      </text>
    </svg>`;
  return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`;
document.body.appendChild(watermakr);

破解与防御

上述方式生成的明水印的破解方法比较简单:打开

Chrome Devtools

找到对应的元素,直接按

delete

即可删除。

如何防御这种呢?JS 有一个方法叫做

MutationObserver

,能够监控元素的改动。

MutationObserver 对现代浏览的兼容性还是不错的,MutationObserver是元素观察器,字面上就可以理解这是用来观察Node(节点)变化的。

需要使用 MutationObserver 监控以下三种情况:

  • 水印元素本身是否被移除;
  • 水印元素属性是否被篡改(display: none …);
  • 水印元素的子元素是否被移除和篡改 (element生成的方式 )。

监听对象为

document.body

, 一旦监听到水印元素被删除,或者属性修改,就重新生成一个。比如,监听水印元素被删除:

/** 观察器的配置(需要观察什么变动)*/
const config = { attributes: true, childList: true, subtree: true };
/** 当观察到变动时执行的回调函数 */
const callback = function (mutationsList, observer) {
  for (let mutation of mutationsList) {
    mutation.removedNodes.forEach(function (item) {
      if (item === watermakr) {
      	document.body.appendChild(watermakr);
      }
    });
  }
};
/** 监听元素 */
const targetNode = document.body; */
/** 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
/** 以上述配置开始观察目标节点 */
observer.observe(targetNode, config);

此方法依然可以被破解:

  1. 打开

    Chrome Devtools

    ,点击设置 – Debugger – Disabled JavaScript,然后再打开页面

    delete

    水印元素;
  2. 复制一个 body 元素,然后将原来 body 元素的删除;
  3. 打开代理工具,将生成水印相关的代码删除。

比如某设计网站的水印内容是以 div 的方式实现的。打开控制台,

Ctrl + F

搜索

watermark

相关字眼。发现直接删除,若没有办法删除水印元素,肯定是利用了

MutationObserver

方法。使用第一个破解方法,将 JavaScript 禁用,再将元素删除。

有一种水印叫暗水印。虽然将一些可见的水印去除了,但是还会存在一些不可见的保护版权的水印。(这就是防止一些坏人拿去作另外的用途)。

暗水印

暗水印是一种肉眼不可见的水印方式,可以保持图片美观的同时,保护资源版权。暗水印的生成方式有很多,常见的为通过修改

RGB 分量值的小量变动

、DWT、DCT 和 FFT 等等方法。

图片都是有一个个像素点构成的,每个像素点都是由 RGB 三种元素构成。当把其中的一个分量修改,人的肉眼是很难看出其中的变化,甚至是像素眼的设计师也很难分辨出。

解码过程

首先拿到图片,进行解码。解码其实很简单,需要根据设定的规律去解码。解密规则是对 R 通道进行处理,R 的分量是奇数则该像素设为红色,R 的分量偶数则该像素设为黑色,

首先创建一个

canvas

标签:

<canvas id="canvas" width="256" height="256"></canvas>
const ctx = document.getElementById('canvas').getContext('2d');
const img = new Image();
let originalData;
img.onload = function () {
  /** canvas像素信息 */
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  processData(ctx, originalData)
};
img.src = 'edemao.png';

打印出这个 originalData 数组,一共有 256 * 256 * 4 = 262144 个值,包括RGBA,A是常用的透明度(alpha 通道)。解码处理函数如下:

const processData = function (ctx, originalData) {
    var data = originalData.data;
    for (let i = 0; i < data.length; i++) {
        if (i % 4 == 0) {
            /** R 分量 */
            if (data[i] % 2 == 0) {
                data[i] = 0;
            } else {
                data[i] = 255;
            }
        } else if (i % 4 == 3) {
            /** alpha通道不做处理 */
            continue;
        } else {
            /** 关闭其他分量,不关闭也不影响解密结果 */
            data[i] = 0;
        }
    }
    /** 将结果绘制到画布 */
    ctx.putImageData(originalData, 0, 0);
}

最后在

img.onload

调用

processData(ctx, originalData)。

function decodeImg(src) {
    const ctx = document.getElementById('canvas').getContext('2d');
    const img = new Image();
    let originalData;
    img.onload = function () {
        /** 获取指定区域的canvas像素信息 */ 
        ctx.drawImage(img, 0, 0);
        originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
        processData(ctx, originalData)
    };
    img.src = src;
}

编码过程

加密呢,首先需要获取加密的图像信息:

let textData;
const ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '30px Microsoft Yahei';
ctx.fillText('edemao', 60, 130);
textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;

然后提取加密信息在待加密的图片上进行处理:

const mergeData = function (ctx, newData, color, originalData) {
    let oData = originalData.data;
    /** offset的作用是找到alpha通道值 */
    let 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;
    }

    for (let i = 0; i < oData.length; i++) {
        if (i % 4 == bit) {
            /** 只处理目标 R 通道 */
            if (newData[i + offset] === 0 && (oData[i] % 2 === 1)) {
                /** 没有信息的像素,该通道最低位置0(奇数),但不要越界超过 255 */
                if (oData[i] === 255) {
                    oData[i]--;
                } else {
                    oData[i]++;
                }
            } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)) {
                /** 有信息的像素,该通道最低位置 1(偶数) */
                oData[i]++;
            }
        }
    }
    ctx.putImageData(originalData, 0, 0);
}

在有像素信息的点,将 R 偶数的通道值 +1。在没有像素点的地方将 R 通道奇数值值转化成偶数。最后在

img.onload

调用 mergeData。

function encodeImg(src) {
  var textData;
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.font = '30px Microsoft Yahei';
  ctx.fillText('edemao', 60, 130);
  textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
  var img = new Image();
  var originalData;
  img.onload = function () {
    /** 获取指定区域的canvas像素信息 */
    ctx.drawImage(img, 0, 0);
    originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
    mergeData(ctx, textData, 'R', originalData)
  };
  img.src = src;
}

但是实际过程需要更专业的加密方式,例如利用傅里叶变化公式,来进行频域制定数字盲水印。

对于网页上展示的图片,一是图片生成的过程中加入的水印,即前端加入的水印。二是物料图本身含有水印。

如何判断是哪一种?可以通过

dom-to-image

这个库,在前端直接进行下载原图,和网页上展示的图片对比是否有大小改变。

攻击暗水印

可以通过以下方式对加了暗水印的图片进行攻击:

  • 加一些元素
  • 截图
  • 加蒙层
  • 改变大小
  • 拍照(最强大的攻击方式,拒绝一切花里胡哨)

其中,只有加蒙层会整体改变通道值,会使得解码后的图片的水印识别能力大大下降,以此达到破解水印的效果。但是,

可见暗水印的抵抗攻击性还是蛮强的,是一种比较好的抵御攻击的方式。



版权声明:本文为qq_42415326原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。