前端水印初探

chensen  |  2019. 03. 06   |  阅读 297 次

为了防止信息泄露,保障信息安全,给网页添加水印是一种比较常见的方法。

本文介绍一种添加水印的方法,具有以下特点:

  • 不影响现有代码
  • 可以任意给网页的不同部分添加水印
  • 纯前端 JavaScript 实现
  • 可简单防止用户通过浏览器开发者工具隐藏水印

思考一下,生成的水印需要满足那些需求呢?

  • 根据动态内容生成静态图片
  • 包含一段标识信息,同时需要覆盖足够的区域

根据水印的需求,自然会想到用 background 指定 image,并让其在 x,y 方向上重复展示,来实现覆盖区域。那么如何根据动态内容生成 image?你一定可以想到前端的绘图魔法(Canvas / SVG)。首先绘制好我们需要的图形,拿到图形编码后的 Data URLs 字符串,该字符串包含所需的图像信息,再配合使用 background 属性实现给网页添加水印。

绘图魔法(一)—— Canvas

HTMLCanvasElement.toDataURL 该方法返回一个包含图片信息的 Data URLs。

const canvas = document.createElement('canvas');  
canvas.setAttribute('width', width);  
canvas.setAttribute('height', height);  
var ctx = canvas.getContext("2d");

ctx.textAlign = textAlign;  
ctx.textBaseline = textBaseline;  
ctx.font = font;  
ctx.fillStyle = fillStyle;  
ctx.rotate(Math.PI / 180 * rotate);  
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);

var base64Url = canvas.toDataURL();  

绘图魔法(二)—— SVG

SVG:可缩放矢量图形是一种基于可扩展标记语言,用于描述二维矢量图形的图形格式。使用 SVG 生成图片的方式和 Canvas 的方式类似,只是 Data URLs 的生成方式换成了 SVG。

const svgStr =  
`<svg xmlns="namespace" width="${width}" height="${width}">
      <text x="50%" y="50%" dy="12px"
        text-anchor="middle"
        stroke="#000000"
        stroke-width="1"
        stroke-opacity="${opacity}"
        fill="none"
        transform="rotate(-45, 120 120)"
        style="font-size: ${fontSize};">
        ${content}
      </text>
</svg>`;  
const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;  

生成 Data URLs 之后,可将其存为全局变量或者存在 store 中,方便随时可取且保证生成一次。通过阅读前文已经知道,水印是可以通过在 DOM 节点上添加 background-imagebackground-repeat 属性来实现的。那么如何给目标节点添加 background-image 属性呢?来看看 Vue、React 项目中如何实现的吧。下文提到的 imageURI 为上文提到的 Data URLs。

Vue

1.自定义指令,在需要打上水印的节点上添加 v-watermarked 指令

Vue.directive('watermarked', {  
  bind(el, binding, vnode) {
    if (binding.value === undefined || !!binding.value) {
      el.style.backgroundImage = imageURI ;
      el.style.backgroundRepeat = 'space repeat';
       /*
       顺手提一波
      background-repeat:space repeat;
      图像在水平方向上尽可能重复,但不会被裁剪,第一个和最后一个图像会被固定在远足的相应的边上,同时空白会均匀地分布在图像之间。图像在垂直方向上重复来覆盖整个区域,若大小不合适可被剪裁 */
    }
  },
  update(el, binding, vnode) {
    if (binding.value === undefined || !!binding.value) {
      el.style.backgroundImage = imageURI;
      el.style.backgroundRepeat = 'space repeat';
    }
  },
});

2.封装组件,在需要打上水印的节点外层包上 <watermark>...</watermark>

<template>  
  <div
    class="watermark"
    :style="{
      backgroundRepeat: 'space repeat',
      backgroundImage:imageURI,
  }">
    <slot />
  </div>
</template>  

react

1.封装组件

<Watermark>  
  ...
</Watermark>  
// 或者使用 render props
<Watermark>  
 {({blob}) => (
    ...
 )}
</Watermark>  

2.直接写进组件样式里面

// 以 styled-components 为例
import styled from 'styled-components';  
const Root = styled.div`  
    ${watermark()}
 `

3.高阶组件

@Watermark
class Page extends React.Component {  
   ...
}

现在你已经大概了解如何给页面打上水印了,你发现有什么问题了吗?其实这存在一个弊端,用户通过开发者工具动态更改 DOM 属性或者结构,就可以轻松删除掉水印。那么我们该如何阻止该行为呢?

MutationObserver

MutationObserver 给开发者们提供了能在某个范围内的 DOM 树发生变化时作出适当反应的能力。

使用 MutationObserver 构造函数,新建一个观察器实例,实例的有一个回调函数,该回调函数接受两个参数,第一个是变动数组(包含一系列变动记录 MutationRecord ),第二个是观察器实例。MutationObserver 的实例的 observe 方法用来启动监听,它接受两个参数。第一个参数:所要观察的 DOM 节点,第二个参数:一个配置对象,指定所要观察的特定变动(config)。

MutationObserver 只能监测到诸如属性改变,增删子节点等,但需要注意的是对于自己本身被删除,是没有办法的,可以用过监测父节点来达到要求。

写个例子吧,可以在监听到用户删除 style 属性操作,及时恢复水印。

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;  
const config = {  
  attributes: true, //观察受监视元素的属性值更改
  attributeOldValue: true, // 记录任何有改动的属性的上一个值
};
const callback = (mutationList, observer) => {  
/*mutationList包含一系列变动记录*/
  mutationList.forEach((mutationRecord) => {
    const { type, attributeName } = mutationRecord;
    /*
    type 更改类型
    attributeName 返回被修改属性的属性名
    */
    if (type === 'attributes' && attributeName === 'style') {
      observer.disconnect(); //先停止监听 否则会不断触发
      const { target, oldValue } = mutationRecord;
      /*
      target 为受监视元素
      oldValue 被更改属性值的旧值
      */
      target.setAttribute('style', oldValue);
      observer.observe(target, config); // 修改完后恢复监听
    }
  });
};
Vue.directive('watermarked', {  
  bind(el, binding, vnode) {
    if (binding.value === undefined || !!binding.value) {
      el.style.backgroundImage = imageURI;
      el.style.backgroundRepeat = 'space repeat';
      const om = new MutationObserver(callback);
      om.observe(el, config);
    }
  },
});

这里提供两个demo,以供参考。

分享到

   
针对web开发者的浏览器缓存指南(译)