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 的优势
- 真正的封装性 - 外部无法通过 DOM API 访问内部元素
- 样式隔离 - 外部 CSS 无法影响 Shadow DOM 内部
- 更难破解 - 需要更高级的技巧才能篡改
方案三:多层水印 + 随机化
通过多层水印和诱饵水印,增加破解难度。
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);
// 使用 !importantstyle.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);}注意事项
⚠️ 前端水印的局限性
-
不是绝对安全的
- 可以被截图、拍照绕过
- 可以被篡改(只是增加难度)
- 无法防止浏览器插件篡改
-
性能影响
- MutationObserver 有一定性能开销
- 定时检测占用 CPU 资源
- 多层水印增加内存占用
-
用户体验
- 水印可能影响阅读体验
- 需要平衡安全性和可读性
✅ 适用场景
- 轻度防护(提醒作用)
- 溯源标识(追踪泄露源)
- 增加泄密成本
- 内部系统防护
🔒 真正安全的方案
对于高安全要求的场景,应该采用:
-
服务端生成带水印的内容
- PDF 水印
- 图片水印
- 视频水印
-
DRM 数字版权保护
- Widevine
- FairPlay
- PlayReady
-
行为监控
- 检测屏幕录制
- 检测截图行为
- 限制复制粘贴
-
物理安全
- 禁止拍照
- 监控摄像头
技术要点总结
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);
// 转为 Base64const dataURL = canvas.toDataURL();Shadow DOM
// 创建 Shadow Hostconst host = document.createElement('div');
// 附加 Shadow Rootconst shadowRoot = host.attachShadow({ mode: 'closed' // 'open' 或 'closed'});
// 在 Shadow Root 中添加内容shadowRoot.innerHTML = '<div>Shadow Content</div>';
// 添加到页面document.body.appendChild(host);mode 区别:
open: 可以通过host.shadowRoot访问closed: 无法从外部访问,更安全
相关资源
- MutationObserver - MDN
- Shadow DOM - MDN
- Canvas API - MDN
- [前端安全最佳实践](https://owasp.org/www-project-web-security-te sting-guide/)
结语
前端水印是一种轻量级的防护手段,主要作用是:
- ✅ 心理威慑 - 让用户知道内容被标记
- ✅ 溯源追踪 - 泄露后可以追溯来源
- ✅ 提高成本 - 增加篡改和泄露的难度
但需要明确的是,前端水印无法提供绝对的安全保障。对于真正重要的内容,应该:
- 🔒 采用服务端水印
- 🔒 使用 DRM 技术
- 🔒 结合物理安全措施
- 🔒 实施严格的权限管理
安全防护是一个纵深防御的体系,前端水印只是其中一环。
前端页面水印实现与防篡改方案
https://fuwari.vercel.app/posts/water-mark/