3113 字
16 分钟
前端页面水印实现与防篡改方案

前端页面水印实现与防篡改方案#

全屏水印是一种常见的安全防护措施,用于防止截图泄露、标识来源等。本文将介绍多种实现方式及完整的防篡改策略。

目录#


方案一:Canvas + Base64 背景图#

基础实现#

这是最常见的实现方式,通过 Canvas 生成水印图片,然后作为背景重复平铺。

class WaterMark {
constructor(options) {
this.options = {
text: '绝密文档',
fontSize: '16px',
fontColor: '#ccc',
width: 200,
height: 150,
rotate: -20,
...options,
};
this.waterMarkDiv = null;
this.observer = null;
this.init();
}
createCanvasImage() {
return new Promise((resolve) => {
const { text, fontSize, fontColor, width, height, rotate } =
this.options;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// 旋转
ctx.translate(width / 2, height / 2);
ctx.rotate((Math.PI / 180) * rotate);
ctx.translate(-width / 2, -height / 2);
// 绘制文字
ctx.font = `${fontSize}px Microsoft YaHei`;
ctx.fillStyle = fontColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, width / 2, height / 2);
resolve(canvas.toDataURL());
});
}
async createWaterMarkDiv() {
const div = document.createElement('div');
const imageUrl = await this.createCanvasImage();
div.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
pointer-events: none;
background-image: url(${imageUrl});
background-repeat: repeat;
`;
div.setAttribute('data-waterMark', 'true');
return div;
}
async init() {
if (this.waterMarkDiv) return;
this.waterMarkDiv = await this.createWaterMarkDiv();
document.body.appendChild(this.waterMarkDiv);
this.startObserver();
}
startObserver() {
const elementObserver = new MutationObserver((mutations) => {
console.log(mutations);
mutations.forEach((mutation) => {
if (
mutation.type === 'childList' ||
mutation.type === 'attributes'
) {
this.restore();
}
});
});
// 监听 body 的子元素变化
const bodyObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.removedNodes.length > 0) {
mutation.removedNodes.forEach((node) => {
// 检测水印是否被删除
if (node === this.waterMarkDiv) {
console.warn('Watermark removed! Restoring...');
this.restore();
}
});
}
});
});
if (this.waterMarkDiv) {
elementObserver.observe(this.waterMarkDiv, {
childList: true,
subtree: true,
attributes: true,
});
}
bodyObserver.observe(document.body, {
childList: true,
});
this.observer = {
elementObserver,
bodyObserver,
disconnect() {
elementObserver.disconnect();
bodyObserver.disconnect();
},
};
}
// 恢复水印
restore() {
this.remove();
// 延迟重新创建,防止被检测
setTimeout(() => {
this.init();
}, 30);
}
// 移除水印
remove() {
if (this.waterMarkDiv && this.waterMarkDiv.parentNode) {
this.waterMarkDiv.parentNode.removeChild(this.waterMarkDiv);
}
this.waterMarkDiv = null;
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
}
const watermark1 = new WaterMark({
text: '机密文档 - 张三',
fontSize: 18,
fontColor: 'rgba(0, 0, 0, 0.52)',
rotate: -25,
});
// 移除水印
// watermark.remove();

方案二:Shadow DOM 封装(更强防护)#

Shadow DOM 提供了真正的封装性,外部 JavaScript 无法直接访问 Shadow Root 内部的元素。

class SecureWatermark {
constructor(options = {}) {
this.options = {
text: '机密文档',
fontSize: 16,
color: 'rgba(0, 0, 0, 0.15)',
width: 200,
height: 150,
rotate: -20,
zIndex: 9999,
...options
};
this.shadowHost = null;
this.shadowRoot = null;
this.observer = null;
this.init();
}
createWatermarkImage() {
const { text, fontSize, color, width, height, rotate } = this.options;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.translate(width / 2, height / 2);
ctx.rotate((Math.PI / 180) * rotate);
ctx.translate(-width / 2, -height / 2);
ctx.font = `${fontSize}px Microsoft YaHei`;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, width / 2, height / 2);
return canvas.toDataURL();
}
init() {
// 创建 Shadow Host
this.shadowHost = document.createElement('div');
this.shadowHost.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: ${this.options.zIndex};
`;
// 创建 Shadow Root(closed 模式,外部无法访问)
this.shadowRoot = this.shadowHost.attachShadow({ mode: 'closed' });
// 创建水印元素
const watermarkDiv = document.createElement('div');
const imageUrl = this.createWatermarkImage();
watermarkDiv.style.cssText = `
width: 100%;
height: 100%;
background-image: url(${imageUrl});
background-repeat: repeat;
`;
this.shadowRoot.appendChild(watermarkDiv);
document.body.appendChild(this.shadowHost);
// 启动防篡改
this.protect();
}
protect() {
// 1. 监听 shadowHost 被删除
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node === this.shadowHost) {
console.warn('Watermark host removed! Restoring...');
setTimeout(() => this.init(), 0);
}
});
});
});
observer.observe(document.body, {
childList: true
});
// 2. 监听样式修改
const styleObserver = new MutationObserver(() => {
this.checkStyle();
});
styleObserver.observe(this.shadowHost, {
attributes: true,
attributeFilter: ['style']
});
// 3. 定时检测
this.intervalId = setInterval(() => {
if (!document.body.contains(this.shadowHost)) {
this.init();
}
this.checkStyle();
}, 2000);
// 4. 冻结对象,防止属性被修改
Object.freeze(this.shadowHost.style);
this.observer = observer;
this.styleObserver = styleObserver;
}
checkStyle() {
const computedStyle = window.getComputedStyle(this.shadowHost);
// 检测关键样式是否被修改
if (
computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
computedStyle.opacity === '0' ||
parseInt(computedStyle.zIndex) < this.options.zIndex
) {
console.warn('Watermark style tampered! Restoring...');
this.restore();
}
}
restore() {
this.remove();
setTimeout(() => this.init(), 0);
}
remove() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
if (this.observer) {
this.observer.disconnect();
}
if (this.styleObserver) {
this.styleObserver.disconnect();
}
if (this.shadowHost && this.shadowHost.parentNode) {
this.shadowHost.parentNode.removeChild(this.shadowHost);
}
}
}

Shadow DOM 的优势#

  1. 真正的封装性 - 外部无法通过 DOM API 访问内部元素
  2. 样式隔离 - 外部 CSS 无法影响 Shadow DOM 内部
  3. 更难破解 - 需要更高级的技巧才能篡改

方案三:多层水印 + 随机化#

通过多层水印和诱饵水印,增加破解难度。

class MultiLayerWatermark {
constructor(options = {}) {
this.options = {
text: '机密文档',
layers: 3, // 多层水印
...options
};
this.watermarks = [];
this.init();
}
init() {
// 创建多层水印
for (let i = 0; i < this.options.layers; i++) {
const watermark = new SecureWatermark({
...this.options,
zIndex: 9999 + i,
// 每层稍微偏移
width: 200 + i * 10,
height: 150 + i * 10,
rotate: -20 + i * 2
});
this.watermarks.push(watermark);
}
// 随机插入干扰水印
this.insertDecoyWatermarks();
}
// 插入诱饵水印(让攻击者以为删除了真实水印)
insertDecoyWatermarks() {
for (let i = 0; i < 2; i++) {
const decoy = document.createElement('div');
decoy.setAttribute('data-watermark', 'decoy');
decoy.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
opacity: 0;
`;
document.body.appendChild(decoy);
}
}
remove() {
this.watermarks.forEach(wm => wm.remove());
this.watermarks = [];
}
}

完整示例:结合所有防护手段#

这是一个集成了所有防护策略的终极水印方案。

class UltimateWatermark {
constructor(options = {}) {
this.options = {
text: '机密文档',
extraText: '', // 额外信息(用户名、时间等)
...options
};
this.init();
}
init() {
// 获取用户信息作为水印
const userInfo = this.getUserInfo();
const text = `${this.options.text}\n${userInfo}`;
// 1. 主水印(Shadow DOM)
this.mainWatermark = new SecureWatermark({
...this.options,
text
});
// 2. 备份水印层
this.backupWatermark = new SecureWatermark({
...this.options,
text,
zIndex: 10000,
color: 'rgba(0, 0, 0, 0.08)'
});
// 3. 防止 CSS 注入
this.protectAgainstCSS();
// 4. 防止 DevTools 删除
this.protectAgainstDevTools();
// 5. 页面可见性检测
this.detectVisibility();
}
getUserInfo() {
const date = new Date().toLocaleString();
const userId = this.options.extraText || 'User123';
return `${userId} - ${date}`;
}
// 防止 CSS 注入覆盖水印
protectAgainstCSS() {
// 添加 !important 样式
const style = document.createElement('style');
style.innerHTML = `
[data-watermark="true"] {
pointer-events: none !important;
opacity: 1 !important;
display: block !important;
visibility: visible !important;
}
`;
document.head.appendChild(style);
// 冻结 style 标签
Object.freeze(style);
}
// 检测 DevTools 打开(简单检测)
protectAgainstDevTools() {
// 检测窗口大小变化(DevTools 打开会改变)
let devtoolsOpen = false;
const threshold = 160;
setInterval(() => {
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
if (widthThreshold || heightThreshold) {
if (!devtoolsOpen) {
devtoolsOpen = true;
console.warn('DevTools detected!');
// 可以发送日志到服务器
this.reportTampering('devtools_detected');
}
} else {
devtoolsOpen = false;
}
}, 1000);
}
// 页面可见性检测
detectVisibility() {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// 页面重新可见时,检查水印是否被破坏
setTimeout(() => {
this.checkIntegrity();
}, 100);
}
});
}
// 检查完整性
checkIntegrity() {
// 检查主水印和备份水印是否存在
const watermarks = document.querySelectorAll('[data-watermark="true"]');
if (watermarks.length < 2) {
console.warn('Watermark integrity compromised!');
this.reportTampering('integrity_check_failed');
this.restore();
}
}
restore() {
this.mainWatermark.restore();
this.backupWatermark.restore();
}
// 上报篡改行为
reportTampering(reason) {
// 发送到服务器记录
fetch('/api/security/watermark-tamper', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reason,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: location.href
})
}).catch(() => {});
}
remove() {
this.mainWatermark?.remove();
this.backupWatermark?.remove();
}
}

使用示例#

// 基础使用
const watermark = new Watermark({
text: '机密文档',
fontSize: 16,
color: 'rgba(0, 0, 0, 0.15)'
});
// Shadow DOM 版本(更安全)
const secureWatermark = new SecureWatermark({
text: '机密文档 - 张三',
fontSize: 18
});
// 终极版本(最强防护)
const ultimateWatermark = new UltimateWatermark({
text: '机密文档',
extraText: 'zhangsan@company.com'
});
// 移除水印
// watermark.remove();

常见破解方式与防御#

破解方式 1:控制台删除 DOM#

攻击代码:

document.querySelector('[data-watermark]').remove();

防御策略:

  • ✅ MutationObserver 监听 DOM 变化
  • ✅ 检测到删除后自动恢复
  • ✅ 多层备份水印

破解方式 2:修改 CSS#

攻击代码:

document.querySelector('[data-watermark]').style.display = 'none';

防御策略:

  • ✅ 监听 style 属性变化
  • ✅ 使用 !important 样式
  • Object.freeze() 冻结样式对象
  • ✅ 定时检测计算样式

破解方式 3:停止 JavaScript 执行#

攻击方式: 在浏览器 DevTools 中暂停 JavaScript 执行

防御策略:

  • ✅ 多层备份水印(部分已经渲染)
  • ✅ CSS 伪元素水印(不依赖 JS)
  • ❌ 前端无法完全防御此类攻击

破解方式 4:修改 Shadow DOM#

攻击代码:

const shadowHost = document.querySelector('div');
shadowHost.shadowRoot.innerHTML = ''; // 失败!mode: 'closed'

防御策略:

  • ✅ 使用 mode: 'closed' 的 Shadow DOM
  • ✅ 外部无法访问 shadowRoot

破解方式 5:CSS 覆盖#

攻击代码:

body::after {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 99999;
}

防御策略:

  • ✅ 多层水印(不同 z-index)
  • ✅ 检测页面异常样式
  • ✅ 最高层水印使用极大的 z-index
  • ⚠️ 无法完全防御(可以用更大的 z-index 覆盖)

实际应用场景#

1. 批量文件下载防护#

// 在文件预览页面添加水印
const watermark = new UltimateWatermark({
text: '内部资料',
extraText: `${currentUser.name} - ${currentUser.email}`
});

2. 在线考试系统#

// 考试页面添加水印,包含考生信息
const watermark = new SecureWatermark({
text: `考生:${student.name}\n准考证号:${student.id}\n${new Date().toLocaleString()}`
});

3. 企业内部系统#

// 所有页面统一添加水印
class GlobalWatermark {
static init() {
if (!window.__watermark__) {
window.__watermark__ = new UltimateWatermark({
text: '公司机密',
extraText: getUserEmail()
});
}
}
}
// 在应用入口调用
GlobalWatermark.init();

最佳实践总结#

前端水印方案优先级#

方案安全级别实现难度推荐场景
DOM 重复平铺简单轻度防护
Canvas 背景图 + MutationObserver⭐⭐中等一般防护
Shadow DOM + 多层防护⭐⭐⭐较高强防护
终极方案(多种技术组合)⭐⭐⭐⭐最高防护

完整防护策略#

┌─────────────────────────────────────┐
│ 前端水印(基础防护) │
│ - Shadow DOM │
│ - MutationObserver │
│ - 多层备份 │
│ - 定时检测 │
│ - CSS 防护 │
│ - 样式冻结 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 服务端水印(终极防护) │
│ - PDF 水印(文档、图片) │
│ - 视频水印(明水印/隐形水印) │
│ - 截图检测(行为分析) │
│ - DRM 数字版权保护 │
└─────────────────────────────────────┘

核心防护要点#

1. DOM 防护#

// MutationObserver 监听
observer.observe(document.body, {
childList: true, // 监听子元素变化
attributes: true, // 监听属性变化
subtree: true // 监听所有后代节点
});

2. 样式防护#

// 冻结样式对象
Object.freeze(element.style);
// 使用 !important
style.opacity = '1 !important';
// 定时检测计算样式
setInterval(() => {
const computed = window.getComputedStyle(element);
if (computed.display === 'none') {
restore();
}
}, 2000);

3. Shadow DOM 封装#

// 使用 closed 模式,外部无法访问
const shadowRoot = element.attachShadow({ mode: 'closed' });

4. 多层备份#

// 创建多个水印层
for (let i = 0; i < 3; i++) {
createWatermark(zIndex + i);
}

注意事项#

⚠️ 前端水印的局限性#

  1. 不是绝对安全的

    • 可以被截图、拍照绕过
    • 可以被篡改(只是增加难度)
    • 无法防止浏览器插件篡改
  2. 性能影响

    • MutationObserver 有一定性能开销
    • 定时检测占用 CPU 资源
    • 多层水印增加内存占用
  3. 用户体验

    • 水印可能影响阅读体验
    • 需要平衡安全性和可读性

✅ 适用场景#

  • 轻度防护(提醒作用)
  • 溯源标识(追踪泄露源)
  • 增加泄密成本
  • 内部系统防护

🔒 真正安全的方案#

对于高安全要求的场景,应该采用:

  1. 服务端生成带水印的内容

    • PDF 水印
    • 图片水印
    • 视频水印
  2. DRM 数字版权保护

    • Widevine
    • FairPlay
    • PlayReady
  3. 行为监控

    • 检测屏幕录制
    • 检测截图行为
    • 限制复制粘贴
  4. 物理安全

    • 禁止拍照
    • 监控摄像头

技术要点总结#

MutationObserver API#

const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log('Mutation type:', mutation.type);
console.log('Mutation target:', mutation.target);
});
});
observer.observe(target, {
childList: true, // 子节点的变化
attributes: true, // 属性的变化
characterData: true, // 节点内容或文本的变化
subtree: true, // 所有后代节点的变化
attributeOldValue: true, // 记录属性旧值
characterDataOldValue: true, // 记录文本旧值
attributeFilter: ['style', 'class'] // 只监听特定属性
});

Canvas 绘制文字#

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置字体
ctx.font = '16px Arial';
// 设置颜色
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
// 设置对齐方式
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 旋转
ctx.rotate(Math.PI / 180 * -20);
// 绘制文字
ctx.fillText('水印文字', x, y);
// 转为 Base64
const dataURL = canvas.toDataURL();

Shadow DOM#

// 创建 Shadow Host
const host = document.createElement('div');
// 附加 Shadow Root
const shadowRoot = host.attachShadow({
mode: 'closed' // 'open' 或 'closed'
});
// 在 Shadow Root 中添加内容
shadowRoot.innerHTML = '<div>Shadow Content</div>';
// 添加到页面
document.body.appendChild(host);

mode 区别:

  • open: 可以通过 host.shadowRoot 访问
  • closed: 无法从外部访问,更安全

相关资源#


结语#

前端水印是一种轻量级的防护手段,主要作用是:

  1. 心理威慑 - 让用户知道内容被标记
  2. 溯源追踪 - 泄露后可以追溯来源
  3. 提高成本 - 增加篡改和泄露的难度

但需要明确的是,前端水印无法提供绝对的安全保障。对于真正重要的内容,应该:

  • 🔒 采用服务端水印
  • 🔒 使用 DRM 技术
  • 🔒 结合物理安全措施
  • 🔒 实施严格的权限管理

安全防护是一个纵深防御的体系,前端水印只是其中一环。

前端页面水印实现与防篡改方案
https://fuwari.vercel.app/posts/water-mark/
作者
Lorem Ipsum
发布于
2025-10-31
许可协议
CC BY-NC-SA 4.0