前 / 后端水印功能实现方案🌊

2,084 阅读3分钟

引言

目前做B端的项目比较多,所以数据安全这块也是重点,最近为项目增加了一个全局水印的功能,维护数据隐私安全。 水印功能从前后端都可以去实现,从后端来实现,可以实现完全的安全,因为在前端是不存在什么安全性的,但是通过前端来实现可以减少服务端的压力,而且性能也是由于后端,所以根据不同的使用场景,我们也可以选择不同的技术方案来实现。

1. 后端水印

我们通过Node.js来实现为图片增加水印的方案,因为在图片这个场景中,资源通过网络请求是直接暴露给用户的,所以使用服务端直接修改数据源的方式,更为适合。

使用Sharptext-to-svg 来实现。将水印处理为Svg,再通过Sharp将源图片与水印图 composite 成新图片。

const sharp = require('sharp');
const TextToSVG = require('text-to-svg');
const textToSVG = TextToSVG.loadSync();

/**
 * 为图片生成水印
 * @param {string} bgImg 背景图片
 * @param {object} font 水印内容属性
 * @param {string} filePath 新图片生成路径
 */
 
// 添加水印
const addWaterMark = async(bgImg, font = {}, filePath) => {
  const { 
    fontSize = 14, 
    text = 'jerry', 
    color = 'rgba(100, 100, 100, 0.10)', 
    left = 0, 
    top = 35
  } = font;

  // 设置文字属性
  const attributes = {
    fill: color
  };

  const options = {
    fontSize,
    anchor: 'top',
    attributes
  };

  // 转成Svg,再转成Buffer
  const svgWaterMarkBuffer = Buffer.from(textToSVG.getSVG(text, options));

  // 写入水印
  await sharp(bgImg || {
    create: {
      width: 200,
      height: 200,
      channels: 4,
      background: { r: 255, g: 255, b: 255, alpha: 0 }
    }
  })
  .rotate(0)
  .resize(200, 200)
  .composite([
    {
      input: svgWaterMarkBuffer,
      top,
      left,
    }
  ])
  .png()
  .toFile(filePath)
  .catch(err => {
    console.log(err)
  });
};

服务端在返回图片资源之前就可以通过这个方法处理图片生成想要的水印,再返回给客户端。

2. 前端水印

我们选择通过前端水印的方式来生成页面的水印,减少服务端的压力,同时也能获得更好的性能。

实现思路主要是通过canvas来生成一个base64的图片来覆盖在页面上,同时通过pointerEvents来禁止各种事件。

let watermark = {}

let setWatermark = (str) => {
  let id = '123456789'

  if (document.getElementById(id) !== null) {
    document.body.removeChild(document.getElementById(id))
  }

  let canvas = document.createElement('canvas')
  canvas.width = 250
  canvas.height = 150

  let ctx = canvas.getContext('2d')
  ctx.rotate(-15 * Math.PI / 150)
  ctx.font = '14px Vedana'
  ctx.fillStyle = 'rgba(100, 100, 100, 0.10)'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'Middle'
  ctx.fillText(str, canvas.width / 8, canvas.height / 2)

  let markNode = document.createElement('div')
  markNode.id = id
  markNode.style.pointerEvents = 'none'
  markNode.style.top = '30px'
  markNode.style.left = '0px'
  markNode.style.position = 'fixed'
  markNode.style.zIndex = '99999'
  markNode.style.width = `${document.documentElement.clientWidth}px`
  markNode.style.height = `${document.documentElement.clientHeight}px`
  markNode.style.background = `url(${canvas.toDataURL('image/png')}) left top repeat`
  document.body.appendChild(markNode)
  
  return id
}

// 该方法只允许调用一次
watermark.set = (str) => {
  let id = setWatermark(str)
  requestAnimationFrame(() => {
    if (document.getElementById(id) === null) {
      id = setWatermark(str).id
    }
  })
  window.onresize = () => {
    setWatermark(str)
  }
}

const outWatermark = (id) => {
  if (document.getElementById(id) !== null) {
    const div = document.getElementById(id)
    div.style.display = 'none'
  }
}
watermark.out = () => {
  const str = '123456789'
  outWatermark(str)
}

export default watermark

但是通过这种方式生成的网页水印,用户可以随意的通过控制台来修改对应 DOM 节点来清除水印,所以这样的水印实现太不理想。我们来使用一个新的Web API MutationObserver MDN中是这样介绍的,它提供了监视对 DOM 树所做更改的能力。

兼容性不错

image.png

具体使用

  // 创建观察器实例并传入回调,也就是我们设置水印的函数
  let observer = new MutationObserver(() => setWatermark(str))
  
  /**
   * 开始监听目标节点
   * @param {boolean}  attributes   元素属性变动
   * @param {boolean}  childList    元素子节点变动
   * @param {boolean}  subtree      元素所有后代节点变动
   */
  observer.observe(document.getElementById('123456789'), { attributes: true, childList: true, subtree: true })
  
  // 水印清除之前调用取消监听,再清除水印
  observer.disconnect();

通过 Web API MutationObserver 就可以实现,监听我们的水印DOM节点,当这个水印的DOM节点发生变化的时候,我们就重新生成新水印,基本限制了用户对水印的随意清除。但是,用户还是有方法可以避开我们的限制,那就是用户直接禁用网页JS,就可以避开我们的限制来消除水印,所以前端是没有安全性可言的,防君子不防小人。

前后端生成水印的方案是各有优劣的,我们应该根据实际的业务场景去选择对应的方案。还是那句话,软件开发没有银弹!