打造更安全的前端水印:防破解技术与暗水印应用

424 阅读6分钟

上一篇我们讲到前端水印实现的几种方式,这一篇我们继续讨论一下如何防止水印被用户恶意去除水印的问题。

在大多数情况下,我们一般都采用 Canvas 实现水印,下面我们就基于此方案上进行讨论,其他方案的防破解策略,本质上是一样的,围绕的核心就是实现水印元素本身的增删改等操作。

因此,在实现 Canvas 水印时,我们需要面对 Canvas 元素属性修改、内容篡改、元素删除、图层覆盖以及感知性问题。本文将逐步探讨每个问题的解决方案,并提供关键的代码片段。

一、水印的防篡改策略

1. 直接删除水印元素

用户发现了水印元素,直接删除 Canvas 元素,从而去除水印,这种方式最简单直接。

面对这种情况,该如何解决呢?

思路:我们可以监听水印元素,发现它被删除后,立刻使内容隐藏或者重新建立水印元素。

因此,我们可以使用 MutationObserver 监控 DOM 变动,确保 Canvas 元素在被删除后能够自动重新创建。

function setupMutationObserver() {
  const targetNode = document.querySelector('.watermarked-content');
  const observer = new MutationObserver(() => {
    // 重新建立水印元素
    createWatermarkCanvas();
  });

  observer.observe(targetNode, { childList: true, subtree: true });
}

2. 修改Canvas 元素属性

第二种场景,既然不能删除,那我就尝试修改它的属性,使它失效。

用户可能通过 CSS 修改 Canvas 元素的属性,或者直接在 HTML 上进行编辑 style 修改,使其隐藏,导致水印不显示。

解决方案: 依然使用 MutationObserver 监听 Canvas 元素的属性变化,确保其始终可见。核心代码如下:

function ensureCanvasVisibility() {
  const canvas = document.querySelector('.watermark-canvas');
  if (canvas) {
    const style = window.getComputedStyle(canvas);
    // 确保 Canvas 始终可见
    if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
      canvas.style.display = 'block';
      canvas.style.visibility = 'visible';
      canvas.style.opacity = '1';
      createWatermarkCanvas(); // 重新绘制水印
    }
  }
}

function setupAttributeObserver() {
  const targetNode = document.querySelector('.watermarked-content');
  const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
        ensureCanvasVisibility();
      }
    });
  });

  observer.observe(targetNode, { attributes: true, subtree: true });
}

3. 覆盖水印图层

第三种情况就是,既然不能删除它,也不能修改它的属性,那就想办法绕过它,转而围攻它的相关的节点,比如它的兄弟节点。

比如修改它的兄弟节点,提升它的图层,使它覆盖 Canvas 元素,从而遮挡水印。如果用户修改 z-index,也可能导致水印无法显示。

解决方案: 确保 Canvas 始终位于最上层,通过 CSS 和 JavaScript 维护 Canvas 的 z-index

核心代码

function ensureCanvasOnTop(defaultMaxZIndex = 1000) {
  const canvas = document.querySelector('.watermark-canvas');
  if (canvas) {
    // 获取所有元素的最高 z-index
    let maxZIndex = Array.from(document.querySelectorAll('body *'))
      .map(el => parseFloat(window.getComputedStyle(el).zIndex))
      .filter(zIndex => !isNaN(zIndex))
      .reduce((max, zIndex) => Math.max(max, zIndex), defaultMaxZIndex);

    // 只有在发现新的最大 z-index 超过默认值时,才更新 canvas 的 z-index
    if (maxZIndex >= defaultMaxZIndex) {
      canvas.style.zIndex = maxZIndex + 1;
    }
  }
}

function setupLayerObserver() {
  const targetNode = document.querySelector('.watermarked-content');
  if (targetNode) {
    const observer = new MutationObserver(() => {
      ensureCanvasOnTop();
    });

    observer.observe(targetNode, { attributes: true, childList: true, subtree: true });
  }
}

// 初始调用,确保 Canvas 一开始就在最上层,使用默认 z-index
ensureCanvasOnTop();

// 设置观察器,监听 DOM 变化
setupLayerObserver();


通过使用 MutationObserver 监听 Canvas 水印的属性和内容变化,可以有效提高水印的防破解能力。效果图如下:

image.png

通过这些策略,可以有效提高 Canvas 水印的安全性,防止其被恶意篡改或删除。

二、暗水印

上面描述的基本都是明水印,也就是用户看得到的,但它也有一定的缺陷,明水印有时会影响用户的体验,特别是一些敏感场合,阻碍用户的阅读。用户感知的情况下,可能会截取没有水印的区域,将绝密文件泄露出去。这时候,暗水印就开始发挥它的作用了。

1. 概念

暗水印(隐形水印)是一种将标识信息嵌入到图像中,而不显著影响图像外观的技术。在处理涉密场景时,添加暗印(水印)可以有效防止内容被未授权访问。

常见的实现暗水印的方式有:

1)透明度隐藏法:原理就是通过改变明水印的透明度,使其极小肉眼不可见,隐藏在网页中,等到要恢复显示时,再利用rgba改变它的色道和透明度,从而实现恢复显示水印内容。

2)最低有效位(LSB)嵌入法:通过修改图像每个像素的最低有效位来嵌入水印。这种方法对图像的可见度影响较小,但可以有效地隐藏信息。

3)层叠透明水印 :在网页中使用多个透明水印层,这些水印层可以跨越多个区块。这种方式通常将水印与内容分离,减少噪声的影响。

4)奇异值分解(SVD) :通过对图像进行奇异值分解,将水印嵌入到奇异值矩阵中。奇异值的微小变化不会显著改变图像外观。

5)嵌入于色彩通道:将水印信息嵌入到图像的某一个色彩通道中(如蓝色通道),因为人的视觉系统对蓝色通道的敏感度较低。等到恢复显示时,再通过特点的算法,进行提取和还原,即可拿到水印内容。

6)结构化水印:利用图像中的某些结构特征(如边缘、纹理)来嵌入水印。可以通过改变图像的纹理模式来隐蔽水印。

2. 暗水印的加码

上述几种办法,这里我们采用层叠透明水印注入的方式,将绘制的水印与内容分离,恢复时减少背景噪声的影响,从而达到更好的恢复显示效果。

使用 HTML5 Canvas 的 globalCompositeOperation 属性可以控制如何将图层合成。通过这种方法,我们可以将水印图层与内容图层分离,并控制它们的合成方式。如果对这个 API 感兴趣的童鞋,可前往developer mozilla了解它。

下面,通过 Canvas 绘制隐蔽的水印信息,使用低透明度或隐藏在图像中的方式实现暗印效果。

核心代码

 function drawWatermark(text = 'user 123', opacity = 0.005) {
    const canvas = document.getElementById('watermarkCanvas');
    const ctx = canvas.getContext('2d');
    const width = window.innerWidth;
    const height = window.innerHeight;

    canvas.width = width;
    canvas.height = height;

    ctx.clearRect(0, 0, width, height);

    ctx.font = '4em Arial';
    ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`; // Semi-transparent color
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    ctx.globalCompositeOperation = 'source-over'; // Default

    // Draw multiple layers of watermark
    for (let y = 0; y < height; y += 150) {
      for (let x = 0; x < width; x += 150) {
        ctx.save();
        ctx.translate(x, y);
        ctx.rotate((-30 * Math.PI) / 180); // Rotate for better effect
        ctx.fillText(text, 0, 0);
        ctx.restore();
      }
    }
  }

实现效果如下:

image.png

上述其实已经将我们的水印内容注入到页面当中了,默认注入 user 123 的暗水印,用户肉眼不可见。

了解了暗水印的注入,那如何提取暗水印的内容呢?

3. 暗水印的解码

假设将上面这个截图发送出去,若要从截图中显示暗印,可以使用图像处理工具或脚本来增强暗印的可见性。如下:

<div class="content">
    <input type="file" id="upload" accept="image/*" />
    <canvas id="canvas" width="500" height="500"></canvas>
    <!-- Fixed canvas size -->
    </div>

<script>
 document.getElementById('upload').addEventListener('change', handleDecode);

 function handleDecode(event) {
    const file = event.target.files[0];
    const img = new Image();
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const reader = new FileReader();

    reader.onload = function (e) {
      img.src = e.target.result;
    };

    img.onload = function () {
      // 重复绘制的次数
      const deCount = 10;
      // const canvasWidth = 500;
      // const canvasHeight = 500;
      const canvasWidth = canvas.width || img.width;
      const canvasHeight = canvas.height || img.height;

      // Clear the canvas
      ctx.clearRect(0, 0, canvasWidth, canvasHeight);

      // Calculate aspect ratio and scaling
      const scaleWidth = canvasWidth / img.width;
      const scaleHeight = canvasHeight / img.height;
      const scale = Math.min(scaleWidth, scaleHeight);

      // Calculate positioning to center the image
      const x = canvasWidth / 2 - (img.width / 2) * scale;
      const y = canvasHeight / 2 - (img.height / 2) * scale;

      // Draw the image on the fixed-size canvas
      ctx.drawImage(img, x, y, img.width * scale, img.height * scale);

      // Enhance the hidden watermark
      ctx.save();
      ctx.globalCompositeOperation = 'overlay'; // Enhance watermark visibility
      ctx.fillStyle = '#000'; // Semi-transparent color for enhancement
      for (let i = 0; i < deCount; i++) {
        ctx.fillRect(0, 0, canvasWidth, canvasHeight);
      }
      ctx.restore();
    };

    reader.readAsDataURL(file);
    }
    </script>

效果图如下:

image.png

user 123就是暗水印内容,但上面的正文内容和暗水印内容都在一起了,不是说水印和正文分离吗?这里说的是解密的过程中,水印是跟正文分离了,如下,ctx内容(暗印内容)重复绘制了10次,次数越多,内容就越清晰。

// 重复绘制的次数
const deCount = 10;
...
ctx.globalCompositeOperation = 'overlay';
for (let i = 0; i < deCount; i++) {
    ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
...

至此,一般水印的内容就基本结束了,但有时我们需要机器帮我们提取暗印的内容。这时,我们就需要用到ocr技术了。

这里是完整的项目地址

三、扩展

一般场景下OCR技术的解决方案有两种,一种是调用公司的OCR服务接口,另一种是调用第三方云服务的OCR接口,比如阿里、腾讯、华为等。

然而调用第三方的会产生一定的费用,如果调用比较频繁的话,可以考虑自己建设OCR服务。

开源项目

目前比较流行的两个开源OCR库,分别为 Tesseract 和 OCRopus。

  1. Tesseract:

    • 适用于一般的OCR任务,尤其是对于印刷文本、简单排版的文档。推荐用于需要跨平台、易用、快速部署的场景。

    • GitHub: Tesseract OCR

    • 官方文档: Tesseract OCR Documentation

  2. OCRopus:

    • 适用于需要处理复杂排版、手写体或自定义模型的OCR任务,适合有较高灵活性和精度要求的应用。

    • GitHub: OCRopus OCR

    • 官方文档: OCRopus Documentation

两者有不同的应用场景,区别总结如下:

特性TesseractOCRopus
开源性免费开源,由 Google 维护免费开源,基于 Python 开发
支持语言支持超过 100 种语言,易于添加新语言默认不支持多语言,需要自行训练模型
识别准确度对印刷文本和清晰图像准确度高对复杂排版和手写体文档识别准确度高
图像质量敏感度对图像噪点、低对比度和模糊较敏感相对较强的抗噪能力,适合处理多样化图像
处理速度对大型文档或高分辨率图像处理速度较慢对复杂版面的文档处理速度较快,但对计算资源要求较高
复杂排版处理对复杂排版(如表格、图形)处理效果一般对复杂排版、混合语言文档有较好的处理能力
使用难度简单易用,有丰富的命令行工具安装和配置复杂,需熟悉 Python 环境
维护和社区拥有活跃的社区,持续更新开发和维护相对较少,更新频率较低
可扩展性支持通过训练数据进行语言扩展,但较为有限模块化设计,用户可自定义处理管道,适用于多种场景
跨平台支持支持 Windows、Linux、macOS 等多种操作系统主要基于 Python,跨平台性取决于 Python 环境

在许多实际应用中,常常会将 OpenCV 与 Tesseract 结合使用。首先使用 OpenCV 进行图像预处理(如去噪、二值化、透视变换),然后使用 Tesseract 进行文本识别,从而获得更高的准确度和稳定性。

下面我们就用 Tesseract 方案实现一个有趣的实验。

实例

之所以称之为有趣,是因为它跟 TensorFlow 一样,Tesseract 也有提供自己的 JS 版本,Tesseract.js 可以在浏览器或 Node.js 中运行。

这是一种非常有趣的尝试。

核心代码如下

<input type="file" id="imageInput" accept="image/*" />
<canvas id="canvas" width="300" height="300"></canvas>
<button id="recognize">Recognize Text</button>
<p id="output">Detected Text:</p>

<script src="https://unpkg.com/tesseract.js@4.1.1/dist/tesseract.min.js"></script>
<script>
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const imageInput = document.getElementById('imageInput');
  let imageData = null;

  // 加载用户上传的图像并显示在 Canvas 上
  imageInput.addEventListener('change', (event) => {
    const file = event.target.files[0];
    const reader = new FileReader();

    reader.onload = function () {
      const img = new Image();
      img.onload = function () {
        // 调整 Canvas 尺寸以适应图像
        canvas.width = img.width;
        canvas.height = img.height;
        // 将图像绘制到 Canvas 上
        ctx.drawImage(img, 0, 0);
        imageData = canvas.toDataURL(); // 保存图像数据
      };
      img.src = reader.result;
    };

    if (file) {
      reader.readAsDataURL(file);
    }
  });

  // 使用 tesseract.js 识别图像中的文本
  document
    .getElementById('recognize')
    .addEventListener('click', async () => {
      if (!imageData) {
        alert('Please upload an image first!');
        return;
      }

      try {
        // 确保 tesseract.js 完全加载
        const result = await Tesseract.recognize(canvas, 'eng', {
          logger: (m) => console.log(m), // 可选的日志输出
        });

        console.log('result', result);

        document.getElementById(
          'output'
        ).innerText = `Detected Text: ${result.data.text.trim()}`;
      } catch (error) {
        console.error('Error recognizing text:', error);
      }
    });
</script>

效果图如下:

image.png 这是一种简单解决方案,适合需要快速集成 OCR 到前端项目的用户。

优点是简单易用。

缺点是识别率低,只能应用于简单场景。

当然,实际业务中还是有很多步骤要处理的,比如处理降噪等,这里只是演示,不再过多赘述,感兴趣的童鞋,赶紧去试试吧。

项目地址:github.com/ctq123/wate…