在公司内部,一些OA系统为了防止信息泄露,往往会添加水印,这类水印需求是广泛而简单的。往往是leader一句话的事情。
作为程序猿(媛)本身开发工作已经很繁重,既然是leader一句话需求,那用简单的100行代码来实现也不为过吧。
不安全的实现
思路:Canvas or Svg + background
这是最简单的水印实现的,原理就是创建一个canvas或svg图片,然后把图片作为body的background。实现代码如下:
export interface ICanvasWM {
container?: HTMLElement;
width?: string;
height?: string;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
font?: string;
globalAlpha?: number;
fillStyle?: string | CanvasGradient | CanvasPattern;
content?: string;
rotate?: number;
zIndex?: number;
}
const defaultParam: ICanvasWM = {
container: document.body,
width: '200px',
height: '200px',
textAlign: 'center',
textBaseline: 'middle',
font: '18px Microsoft Yahei',
globalAlpha: 0.04,
fillStyle: 'black',
content: '请勿外传',
rotate: 30,
zIndex: 1000,
};
const MAX_RECOVER_TIME = 100;
let recoverTime = 0;
/**
* canvas 实现水印
* @param param: ICanvasWM
*/
function CanvasWM(param: ICanvasWM = defaultParam): void {
const wmParam = Object.assign(defaultParam, param);
const { container, width, height, textAlign, textBaseline, font, fillStyle, globalAlpha, content, rotate, zIndex }
= wmParam;
const canvas = document.createElement('canvas');
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
const ctx: CanvasRenderingContext2D = canvas.getContext('2d');
ctx.textAlign = textAlign as CanvasTextAlign;
ctx.textBaseline = textBaseline as CanvasTextBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.globalAlpha = globalAlpha;
ctx.rotate((Math.PI / 180) * rotate);
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
const base64Url = canvas.toDataURL();
const wmDom = document.querySelector('.__wm');
const watermarkDiv = wmDom ?? document.createElement('div');
const styleStr = `
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
z-index:${zIndex};
pointer-events:none;
background-repeat:repeat;
background-image:url('${base64Url}')`;
watermarkDiv.setAttribute('style', styleStr);
watermarkDiv.classList.add('__wm');
if (!wmDom) {
container.style.position = 'relative';
container.insertBefore(watermarkDiv, container.firstChild);
}
}
export default CanvasWM;
- 优点:简单,去掉ts类型,60行代码搞定
- 缺点:绝对的不安全,F12打开页面源,手动删除body的background属性就能消除水印
安全的实现
思路:Canvas or Svg + MutationObserver
只需在不安全的方法基础上增加 MutationObserver
监听,重新覆盖用户对水印dom
的修改动作,就能达到用户无法通过修改dom
而去除水印的目的。
附上完整代码:
export interface ICanvasWM {
container?: HTMLElement;
width?: string;
height?: string;
textAlign?: CanvasTextAlign;
textBaseline?: CanvasTextBaseline;
font?: string;
globalAlpha?: number;
fillStyle?: string | CanvasGradient | CanvasPattern;
content?: string;
rotate?: number;
zIndex?: number;
}
const defaultParam: ICanvasWM = {
container: document.body,
width: "200px",
height: "200px",
textAlign: "center",
textBaseline: "middle",
font: "18px Microsoft Yahei",
globalAlpha: 0.04,
fillStyle: "black",
content: "请勿外传",
rotate: 30,
zIndex: 1000,
};
const MAX_RECOVER_TIME = 100;
let recoverTime = 0;
/**
* canvas 实现水印
* @param param: ICanvasWM
*/
function CanvasWM(param: ICanvasWM = defaultParam): void {
const wmParam = Object.assign(defaultParam, param);
const {
container,
width,
height,
textAlign,
textBaseline,
font,
fillStyle,
globalAlpha,
content,
rotate,
zIndex,
} = wmParam;
const canvas = document.createElement("canvas");
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
const ctx: CanvasRenderingContext2D = canvas.getContext("2d");
ctx.textAlign = textAlign as CanvasTextAlign;
ctx.textBaseline = textBaseline as CanvasTextBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.globalAlpha = globalAlpha;
ctx.rotate((Math.PI / 180) * rotate);
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
const base64Url = canvas.toDataURL();
const wmDom = document.querySelector(".__wm");
const watermarkDiv = wmDom ?? document.createElement("div");
const styleStr = `
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
z-index:${zIndex};
pointer-events:none;
background-repeat:repeat;
background-image:url('${base64Url}')`;
watermarkDiv.setAttribute("style", styleStr);
watermarkDiv.classList.add("__wm");
if (!wmDom) {
container.style.position = "relative";
container.insertBefore(watermarkDiv, container.firstChild);
}
const { MutationObserver } = window;
if (MutationObserver) {
let mo = new MutationObserver(() => {
const wmDomOld = document.querySelector(".__wm");
// 只在__wm元素变动才重新调用 CanvasWM
if (
((wmDomOld && wmDomOld.getAttribute("style") !== styleStr) ||
!wmDomOld) &&
recoverTime < MAX_RECOVER_TIME
) {
// 避免一直触发
mo.disconnect();
mo = null;
recoverTime += 1;
CanvasWM(wmParam);
}
});
mo.disconnect();
mo.observe(container, {
attributes: true,
subtree: true,
childList: true,
});
}
}
export default CanvasWM;
Double安全的方式
思路:Shadow Dom + MutationObserver
为了提高水印的隐蔽性,同时避免受外部代码影响,从而在一定程度上防止篡改,可以考虑把水印元素放在 Shadow DOM
中。
来看下 Shadow DOM
的基本用法。使用 Element.attachShadow()
方法来将一个 shadow root
附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
。open
表示可以通过页面内的 JavaScript
方法来获取 Shadow DOM
。而 closed
则表示不可以从外部获取 Shadow DOM
。
const shadowDom = Element.attachShadow({mode: 'closed'});
shadowDom.innerHTML = xxx
实现基本和第二种方式一致,只是把水印挂载点换成了Shadow DOM
,详细代码就不再贴出了。
总结
- 本文讲的都是可见水印的实现方式,至于”盲水印“的实现大家可以参考网上其他资料;
- 安全水印的核心是
MutationObserver
API,用于监听用户动作,防止删除/篡改水印节点; - 使用
Shadow DOM
做安全加固是为了进一步提高找到水印节点的成本,并且防止外部样式影响到水印。