你需要知道的,前端水印实现方案?

3,191 阅读5分钟

**目的 :**从概念,业务场景上理解水印,作为前端开发者能快速提出水印方案并实现,熟悉实现原理及相关技术;

什么是水印?

使用水印解决什么问题?

  • 为了防止信息泄露知识产权被侵犯,在web的世界里,对于图片文档等增加水印处理是十分有必要的。
  • 说人话:****对于某些机密文档或者内部文件,当文档外流的时,可以通过水印追究到责任人;

应用的业务场景?

标记原创或来源

一个资源绑定一个固定用户,一般创建时处理一次,标记原创或来源,如掘金;

如下,为掘金水印:

标记查看者

一个资源关联多个不同用户,每个用户查看时都要处理,标记当前查看者,如飞书;

常见水印分类 & 特点

按添加环境分类:前端水印(浏览器环境添加),后端水印(服务环境添加)

从实现方式分类:显性水印,数字水印

前端方案

  • 不占用服务器运算量内存,能够快速响应请求

  • 安全性较低,有心人容易通过各种骚操作获取到源文件

后端方案

  • 安全性较高,无法获取到源文件

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

显性水印

  • 容易处理,算法较为简单

  • 攻击者就可以通过裁剪、模糊等操作对水印进行攻击消除,同时显性水印也会破坏图片的完整性

数字水印

  • 算法一般较为复杂

  • 抗攻击能力较强

实现方案

显性水印 + Canvas + 保护程序

实现:定时器监听或MutationObserver,提醒用户操作违法,并且删除掉水印,并且重新生成水印

攻击:完全可以拷贝下来整个已经渲染完成的HTMLandCSS,去除JS的保护干扰,故而,此方案只防住了君子和小白

显性水印 + Canvas + base64

  • 给现有图片打水印:

用canvas去绘制图片,加上水印文字,然后转成base64,展示在页面中,使html中的img资源是加过水印的base64;

  • 上传图片打水印:

文件选择后,拿到文件对象,把文件转成base64放到img标签中预览,点击确定上传时,将图片用canvas绘制下来,绘制水印,把canvas转成base64,再转成二进制文档上传;

如何选择方案?

没有最好的方案,只有根据环境与需求,使用当前最适合的方案,安全性、性能、实现成本之间权衡利弊。

实战(显性水印 + Canvas + 保护程序)

开始之前需要弄清楚的几个问题:

水印放在哪里?

盖在内容最上层的(z-index:999),并且要能穿透事件(pointer-events: none; 元素永远不会成为鼠标事件的target)

什么是Canvas?

一句话解释,一个可以使用 JavaScript 等脚本语言向其中绘制图像的 HTML 标签,应用于图表,动画,游戏等;canvas是画布,js是画笔;

Canvas常用API

// 建立并返回一个二维渲染上下文,contextType 为 webgl 时,可以建立一个三维上下文
canvasElement.getContext("2d") 

// 状态相关
ctx.save(); // 存储当前状态
ctx.restore(); // 恢复到最近save的状态

// 绘制矩形
ctx.clearRect(x,y,w,h); // 以x,y为起点,绘制宽w,高h的的透明矩形
ctx.fillRect(); // 以x,y为起点,绘制宽w,高h的的实心矩形
ctx.strokeRect(); // 以x,y为起点,绘制宽w,高h的的描边矩形

// 绘制图片
ctx.drawImage(image,x,y,w,h); // image 图片元素,x,y为起点绘制

// 绘制文本相关
ctx.fillText(this.text, 10, 0);// 在(x,y)位置绘制(填充)文本
ctx.strokeText(this.text, 10, 0); // 在(x,y)位置绘制(描边)文本--空心文本

// 字体设置
ctx.font = "normal " + this.fontSize + "px Microsoft Yahei";

// 填充描边样式
ctx.fillStyle = this.color; // 设置填充颜色
ctx.strokeStyle = this.color; // 设置描边颜色

// 变换相关
ctx.translate(boxWidth * i, j * boxHeight - top); // 平移
ctx.rotate((-30 * Math.PI) / 180); // 旋转,用弧度 
// 180 度(degree) = 3.141593 弧度(radian)
ctx.scale(x,y) // x,y 宽高缩放

所谓的保护程序是啥?

监听dom变化,而重新生成水印;

MutationObserver是什么?

一个web API 接口,提供了监视对DOM树更改的能力;

代码实现参考:

import { Component, Vue, Prop, Watch } from "vue-property-decorator";
import localStorage from "@/utils/localStorage";
@Component({})
export default class Watermark extends Vue {
  @Prop() content;

  id = "watermark-canvas";
  color = "#000";
  fontSize = 20;
  width = window.screen.width;
  height = window.screen.height;
  ccsText = "position: fixed; z-index: 999; pointer-events: none; opacity: 0.1;";
  text = "水印文案";
  observer = null;

  clearCanvas() {
    const oldCanvas = document.getElementById(this.id);
    if (oldCanvas) {
      oldCanvas.parentNode.removeChild(oldCanvas);
    }
  }

  createCanvas() {
    //创建画布
    const body = document.getElementsByTagName("body");
    const canvas = document.createElement("canvas");
    const id = this.id;
    //设置画布id
    canvas.setAttribute("id", id);
    canvas.width = this.width;
    canvas.height = this.height;
    canvas.style.cssText = this.ccsText;
    body[0].appendChild(canvas);
  }

  draw() {
    const canvasElement = document.getElementById(this.id);
    const ctx = canvasElement.getContext("2d");
    const { width: textWidth } = ctx.measureText(this.text);
    const boxHeight = 180;
    const boxWidth = textWidth + 200;
    const yCount = parseInt(this.height / boxHeight); // 行数
    const xCount = parseInt(this.width / boxWidth) + 1; // 最多产生多少个
    const top = 50;
    for (let i = 0; i < xCount; i++) {
      for (let j = 0; j < yCount; j++) {
        ctx.save();
        ctx.translate(boxWidth * i, j * boxHeight - top);
        ctx.rotate((-30 * Math.PI) / 180);// 角度转换 2PI = 360度 
        ctx.fillStyle = this.color;
        ctx.font = "normal " + this.fontSize + "px Microsoft Yahei";
        ctx.fillText(this.text, 10, 0);
        ctx.restore();
      }
    }
  }

  init() {
    this.clearCanvas();
    this.clearLock();
    this.createCanvas();
    this.draw();
    this.safetyLock();
  }

  created() {
    this.text && this.init();
  }

  safetyLock() {
    // 选择需要观察变动的节点
    const targetNode = document.getElementById(this.id);

    // 观察器的配置(需要观察什么变动)
    const config = { attributes: true, childList: true, subtree: true };

    // 当观察到变动时执行的回调函数
    const callback = (mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.type === "childList") {
          console.log("A child node has been added or removed.");
          this.init();
        } else if (mutation.type === "attributes") {
          console.log(
            "The " + mutation.attributeName + " attribute was modified."
          );
          this.init();
        }
      }
    };

    // 创建一个观察器实例并传入回调函数
    const observer = new MutationObserver(callback);

    // 以上述配置开始观察目标节点
    observer.observe(targetNode, config);

    this.observer = observer;
  }

  clearLock() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  destroyed() {
    this.clearCanvas();
    this.clearLock();
  }
}

思考

你能在画布上画出这样的图吗?

更多

看看飞书水印实现

数字水印见下面文档:

数字水印技术在前端落地的思考[0] - 概念篇

阿里巴巴公司根据截图查到泄露信息的具体员工的技术是什么? - 知乎