最近公司的系统要升级,原来的水印部分也是我做的,用的是最简单的Canvas实现的。
效果其实不错,之前用着也一直没啥问题。
但是客户那边最近好像有个懂技术的人加入,说这种水印不太合规。
正好借着系统设计让我们把这里给改了。
为什么普通水印一删就没?
之前做水印,其实无外乎两个方案,要么是背景图(CSS),要么是 Canvas/SVG 生成的元素(JS)。
CSS水印:直接在 F12 中找到对应样式,删除 background-image 就没了。- 普通
Canvas/SVG水印:找到对应的 DOM 节点,右键删除,水印直接消失。
就是说在前端其实可以隐藏掉相应的水印。
MutationObserver 防篡改水印
MutationObserver 是浏览器原生提供的 DOM 变化监听 API,用于监听 DOM 节点的增删、属性修改、子树变化,触发变化后执行自定义回调。
核心就是监控这个水印节点,只要它被删、被改,就立刻自动重建。
形成"删一次、建一次"的死循环,这也是它删不掉的关键。
这个 API 是浏览器原生支持的,不需要引入任何第三方插件,兼容性覆盖所有现代浏览器(IE 除外,现在新系统的开发基本不考虑 IE 了)。

完整代码
这里基于 SVG 实现,比 Canvas 更简洁、无跨域问题。
包含水印生成、挂载、监听全流程,兄弟们可以直接集成到项目中。
最终效果:
-
全屏斜纹水印,不影响页面点击、输入;
-
F12 删除水印节点 → 自动恢复;
-
修改水印样式(display: none、opacity: 0)→ 自动恢复;
-
修改水印层级(z-index)→ 自动恢复。
js
// 生成水印(SVG 格式,简洁无跨域)
function createWatermark() {
const watermarkText = "这是水印信息";
const fontSize = 16; // 水印字体大小
const opacity = 0.1; // 水印透明度(0-1,越小越淡)
const angle = -20; // 水印倾斜角度(负号为向左倾斜)
const gapX = 220; // 水印横向间距
const gapY = 200; // 水印纵向间距
// SVG 水印模板(无需修改,只改上面的配置即可)
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${gapX}" height="${gapY}" style="opacity:${opacity}">
<text x="0" y="50" font-size="${fontSize}" fill="#000" transform="rotate(${angle}, 0, 50)">
${watermarkText}
</text>
</svg>
`;
// 转 base64 格式,避免跨域问题
const base64 = btoa(unescape(encodeURIComponent(svg)));
return `data:image/svg+xml;base64,${base64}`;
}
// 创建并挂载水印遮罩层(核心容器)
function initWatermark() {
// 先删除旧水印,避免重复生成
const oldWm = document.getElementById('__watermark_protect');
if (oldWm) oldWm.remove();
// 创建水印容器
const wmDiv = document.createElement('div');
wmDiv.id = '__watermark_protect'; // 水印唯一标识,用于监听
// 关键样式:全屏固定、最高层级、不影响点击
wmDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999999; /* 最高层级,避免被其他元素覆盖 */
pointer-events: none; /* 灵魂属性:不阻挡页面点击、输入 */
background-image: url(${createWatermark()});
background-repeat: repeat; /* 平铺水印 */
`;
// 挂载到页面 body 中
document.body.appendChild(wmDiv);
return wmDiv;
}
// MutationObserver 监听防篡改
function observeWatermark() {
const wmDiv = initWatermark(); // 初始化水印
// 监听配置:监听哪些 DOM 变化
const config = {
childList: true, // 监听子节点增删(比如水印被删除)
attributes: true, // 监听属性修改(比如样式被改)
subtree: true, // 监听整个子树变化(避免漏监)
attributeOldValue: true, // 记录属性修改前的值(可选,用于复杂判断)
};
// 监听回调:一旦检测到变化,执行修复逻辑
const callback = (mutationsList) => {
// 遍历所有变化,判断是否是水印被破坏
for (let mutation of mutationsList) {
const currentWm = document.getElementById('__watermark_protect');
// 触发修复的条件:水印节点不存在,或节点被替换
if (!currentWm || currentWm !== wmDiv) {
observeWatermark(); // 重新初始化水印+监听
break; // 跳出循环,避免重复触发
}
}
};
// 创建监听器,监听 body 下的所有变化
const observer = new MutationObserver(callback);
observer.observe(document.body, config);
}
// 启动防篡改水印(页面加载完成后执行)
window.onload = observeWatermark;
优化建议
这个方案从原理上来说是绝对的防篡改的,但是也不是万能的。
它监听DOM的变化,所以DOM上的操作基本上都破解不了这个水印。
但是如果通过关闭 Js,然后去掉水印,这个防不住。
或者是抓包 的形式,以及通过注入代码 禁用掉 MutationObserver,这种都是防不住的。
所以系统级别 的水印可以用这个,但是文件级别的我还是推荐后端生成。
也就是说后端回传的PDF本身就是带着水印来的,这样能最大限度仿破解。
总结
MutationObserver 的核心原理就是通过监听 DOM 变化,水印被破坏就自动重建,实现前端层面的防篡改。
但是要记住的是:天底下没有绝对安全的系统。