在HTML页面中添加水印是常见的标识需求,但普通的水印很容易被用户通过浏览器开发者工具删除,导致防护效果失效。要解决这个问题,需要从水印生成方式和节点监控两方面入手,提升水印的抗删除能力。

普通水印容易被删除的原因
普通的水印通常是直接通过<div>标签添加到页面中,样式固定且节点独立,用户打开开发者工具后,很容易找到对应的DOM节点直接删除,或者通过CSS隐藏节点,这种方式的水印几乎没有防护能力。
防止水印被删除的常用技巧
1. 使用MutationObserver监听DOM变化
MutationObserver可以监听指定DOM节点的子节点、属性等变化,当水印节点被删除或修改时,可以立即重新生成水印,恢复水印状态。
// 创建水印节点
function createWatermark() {
const watermark = document.createElement('div');
watermark.id = 'page-watermark';
watermark.style.position = 'fixed';
watermark.style.top = '0';
watermark.style.left = '0';
watermark.style.width = '100%';
watermark.style.height = '100%';
watermark.style.pointerEvents = 'none';
watermark.style.opacity = '0.1';
watermark.style.fontSize = '24px';
watermark.style.color = '#000';
watermark.style.display = 'flex';
watermark.style.flexWrap = 'wrap';
watermark.style.alignItems = 'center';
watermark.style.justifyContent = 'center';
watermark.style.zIndex = '9999';
// 填充水印内容
watermark.innerHTML = '内部资料 禁止外传 '.repeat(100);
document.body.appendChild(watermark);
return watermark;
}
// 初始化水印
let watermarkNode = createWatermark();
// 监听body的DOM变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// 如果水印节点被删除
if (mutation.removedNodes.length > 0) {
for (let node of mutation.removedNodes) {
if (node.id === 'page-watermark') {
// 重新创建水印
watermarkNode = createWatermark();
break;
}
}
}
// 如果水印属性被修改(比如隐藏)
if (mutation.type === 'attributes' && mutation.target.id === 'page-watermark') {
const target = mutation.target;
if (target.style.display === 'none' || target.style.opacity === '0') {
// 恢复水印样式
target.style.display = 'flex';
target.style.opacity = '0.1';
}
}
});
});
// 开始监听
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});2. 使用Canvas生成水印并转为背景图
通过Canvas生成水印图片,将图片设置为页面背景,这种方式的水印不是独立的DOM节点,用户很难通过删除单个节点来清除水印。
function setCanvasWatermark(text) {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;
const ctx = canvas.getContext('2d');
// 设置水印样式
ctx.rotate(-20 * Math.PI / 180);
ctx.font = '16px Arial';
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillText(text, 50, 100);
// 转为背景图
const bgUrl = canvas.toDataURL('image/png');
document.body.style.backgroundImage = `url(${bgUrl})`;
document.body.style.backgroundRepeat = 'repeat';
}
// 调用方法设置水印
setCanvasWatermark('内部资料 禁止外传');3. 定时校验水印状态
除了监听DOM变化,还可以设置定时器定时检查水印是否存在、样式是否正常,发现异常就重新生成水印,作为额外的防护手段。
// 定时校验水印
setInterval(() => {
const watermark = document.getElementById('page-watermark');
if (!watermark) {
// 水印不存在,重新创建
createWatermark();
} else if (watermark.style.display === 'none' || watermark.style.opacity === '0') {
// 水印被隐藏,恢复样式
watermark.style.display = 'flex';
watermark.style.opacity = '0.1';
}
}, 1000);4. 将水印节点深度嵌套
不要将水印节点作为body的直接子节点,而是将其嵌套在页面的核心内容节点内部,增加用户查找和删除水印节点的难度。
// 将水印嵌套在内容容器内
function createNestedWatermark() {
const contentContainer = document.getElementById('main-content');
if (!contentContainer) return;
const watermark = document.createElement('div');
watermark.id = 'nested-watermark';
// 设置水印样式,覆盖容器范围
watermark.style.position = 'absolute';
watermark.style.top = '0';
watermark.style.left = '0';
watermark.style.width = '100%';
watermark.style.height = '100%';
watermark.style.pointerEvents = 'none';
watermark.style.opacity = '0.1';
watermark.style.fontSize = '20px';
watermark.style.color = '#000';
watermark.style.zIndex = '999';
watermark.innerHTML = '内部资料 '.repeat(50);
contentContainer.appendChild(watermark);
}不同方案的适用场景
如果页面内容比较简单,优先选择Canvas背景水印,防护效果好且性能影响小;如果是复杂的管理系统页面,可以结合MutationObserver和定时校验的方案,多重防护提升水印的稳定性。需要注意的是,前端的水印防护只能增加删除难度,无法做到绝对无法删除,重要内容的保护还需要结合后端权限控制等手段。