🛡️ 前端水印方案全解析——从「能被 F12 删掉」到「删不掉」的反破解实战

冷门知识点 | 安全防护 | 领导要求加个水印保护公司敏感信息,你两分钟写完 position: fixed + opacity: 0.1,结果运营同事打开 DevTools 一键删除......今天就把水印做到「删不掉」。


问题场景

运营团队正在使用一个内部运营后台,页面上展示着千万级 GMV 数据。安全部门要求所有数据页面必须带上员工工号水印。

第一天:PM:「加个水印」 你:用 position: fixeddiv 覆盖全屏,加 pointer-events: none,搞定。

第二天:运营反馈:「我用 F12 把水印那个 div 删掉了,截图发给老板了......没问题吧?」 你:「......」

内核中 CSS-only 水印在 DevTools 面前就是纸糊的。看似「有」实则「无」,用户只需随手删一个 DOM 节点就破了。


原因分析

前端水印常见的「纸上谈兵」实现方式:

html 复制代码
<!-- ❌ 最脆弱的水印 -->
<div id="watermark" style="position:fixed;inset:0;pointer-events:none;opacity:0.08;z-index:99999;">
  <span>机密-张三-2026-07-23</span>
</div>

三大致命伤:

  1. DOM 可删除 --- DevTools 选中节点按 Delete 就没了
  2. DOM 可隐藏 --- display: nonevisibility: hidden 一键隐身
  3. CSS 可覆盖 --- 修改 z-indexopacity 直接失效

更「高级」一点的多层水印 + 脚本定时重绘,也只能防住普通用户。只要懂一点 JS debugging(断点除定时器、删除 DOM),一样破防。


解决方案

方案一:Canvas 全屏覆盖(初级加固)

用 Canvas 绘制水印渲染为背景图片,通过 background-repeat 铺满,DOM 路径上无从删除。

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <style>
    .watermarked {
      /* 背景图将在 JS 中设置 */
      background-repeat: repeat;
      background-size: 200px 150px;
    }
  </style>
</head>
<body id="app">
  <div>敏感数据区域</div>
  <script>
    function createWatermark(text = '机密-张三-2026-07-23') {
      const canvas = document.createElement('canvas');
      canvas.width = 200;
      canvas.height = 150;
      const ctx = canvas.getContext('2d');
      
      ctx.rotate(-20 * Math.PI / 180);  // 旋转 -20 度
      ctx.font = '14px Microsoft YaHei';
      ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
      ctx.fillText(text, 20, 80);
      
      document.getElementById('app').style.backgroundImage =
        `url(${canvas.toDataURL('image/png')})`;
    }
    createWatermark();
  </script>
</body>
</html>

优点: DOM 树里找不到水印元素,无法「Delete」删除 缺点: 修改 background-image CSS 属性即可覆盖,或截图后简单 PS 擦除


方案二:Canvas + MutationObserver 双重监听(中级防护)

在方案一基础上,加入 MutationObserver 监听 DOM/样式篡改,一旦发现水印被篡改立即重建。

javascript 复制代码
function setupWatermark(text) {
  const target = document.getElementById('app');
  let watermarkStyleId = 'wm-style';

  function applyWatermark() {
    const canvas = document.createElement('canvas');
    canvas.width = 200;
    canvas.height = 150;
    const ctx = canvas.getContext('2d');
    ctx.rotate(-20 * Math.PI / 180);
    ctx.font = '14px Microsoft YaHei';
    ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
    ctx.fillText(text, 20, 80);
    
    target.style.backgroundImage = `url(${canvas.toDataURL()})`;
  }

  // 第一次应用
  applyWatermark();

  // MutationObserver 监听 background-image 是否被修改
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.type === 'attributes' &&
          mutation.attributeName === 'style') {
        const bg = target.style.backgroundImage;
        if (!bg || bg === 'none') {
          applyWatermark(); // 被删了?马上恢复!
        }
      }
    }
  });

  observer.observe(target, {
    attributes: true,
    attributeFilter: ['style'],
  });

  // 额外防御:定时检查(防 MutationObserver 被断开)
  const interval = setInterval(() => {
    const bg = target.style.backgroundImage;
    if (!bg || bg === 'none') {
      applyWatermark();
    }
  }, 1000);

  // 对外暴露清理函数
  return () => {
    observer.disconnect();
    clearInterval(interval);
  };
}

const cleanup = setupWatermark('机密-张三-2026-07-23');

增加了三层防御: 1️⃣ MutationObserver 监听 style 属性变更 → 篡改后即时恢复 2️⃣ 定时轮询兜底 → 防 Observer 被断开 3️⃣ 水印恢复在同一个微任务/宏任务内 → 截图都来不及


方案三:CSS ::before/::after + Canvas Blob 的深层防御(高阶加固)

利用伪元素 content: url(...) 嵌入水印图片,配合 !importantpointer-events: none

javascript 复制代码
function createDeepWatermark(text) {
  const canvas = document.createElement('canvas');
  canvas.width = 400;
  canvas.height = 300;
  const ctx = canvas.getContext('2d');
  ctx.rotate(-20 * Math.PI / 180);
  ctx.font = '16px Microsoft YaHei';
  ctx.fillStyle = 'rgba(0, 0, 0, 0.06)';
  ctx.fillText(text, 40, 160);
  
  // 转为 Blob URL
  const dataUrl = canvas.toDataURL();
  
  // 注入一个带 !important 的样式,通过伪元素渲染
  const style = document.createElement('style');
  style.id = 'wm-core-style';
  style.textContent = `
    #app::before {
      content: '' !important;
      position: fixed !important;
      inset: 0 !important;
      background-image: url("${dataUrl}") !important;
      background-repeat: repeat !important;
      pointer-events: none !important;
      z-index: 2147483647 !important;
      opacity: 1 !important;
      display: block !important;
      visibility: visible !important;
    }
  `;
  document.head.appendChild(style);
  
  // 同样加上 MutationObserver 和定时器
  // ...(复用方案二逻辑)
}

为什么要用 ::before 伪元素?

  • DevTools 里无法直接「选中删除」伪元素
  • 即使删掉 <style> 标签轮询也能恢复
  • !important 压过大部分用户注入的样式

方案四:防御 Degreed 用户(高阶反调试)

针对会打断点、手动删 Observer 的高级用户:

javascript 复制代码
// 检测 DevTools 是否打开(简易版)
const devtools = /./;
devtools.toString = function() {
  applyWatermark(); // 每次在 Console 求值时重建水印
  return '';
};

// 重写 MutationObserver 的 disconnect 方法
const originalDisconnect = MutationObserver.prototype.disconnect;
MutationObserver.prototype.disconnect = function() {
  // 判断是否是我们的 observer 被断开
  if (this._isWatermarkObserver) {
    console.warn('⚠️ 水印防护: 禁止断开 Observer');
    return;
  }
  return originalDisconnect.call(this);
};

⚠️ 注意: 这种「反反调试」手段需要把握好度,过度反调试会影响正常开发体验,建议仅在内部敏感系统中使用。


要点总结

方案 防护等级 实现成本 用户能否破解
CSS fixed 覆盖 5 分钟 1 秒
Canvas 背景图 ⭐⭐ 15 分钟 需要改 CSS
Canvas + Observer ⭐⭐⭐⭐ 30 分钟 需要断点调试
深度防御全栈 ⭐⭐⭐⭐⭐ 1 小时 需要极高技术水平

实战建议:

  • 普通后台管理系统:方案二(Canvas + Observer)就够用了,覆盖 95% 用户
  • 高安全级别场景:方案三 + 方案四组合
  • 永远别只依赖前端水印:前端水印是「防君子不防小人」的威慑手段,真正保密靠后端水印(接口注入 + 图片服务器打码)
  • 截图的终极防御 :前端水印 + Document.exitFullscreen() 监听 + 页面失焦模糊处理 + 禁用右键/打印
  • 性能注意 :Canvas 生成水印不要每次都 toDataURL,缓存一次结果;Observer 回调尽量轻量,避免高频触发

最后一句大实话: 用户真想扒你的数据,截图后裁剪掉水印发给 AI 擦除就行了。前端水印的核心价值是 「事后追溯」------每张泄露的截图上都有工号水印,安全部门就能找到「内鬼」。在 DOM 层面删掉水印只是「掩耳盗铃」,服务器端可以通过 HTTP 日志 + 请求指纹精确定位到人。

相关推荐
学网安的肆伍2 小时前
【046-WEB攻防篇】注入工具&SQLMAP&Tamper编写&指纹修改&高权限操作&目录架构
前端·架构
Highcharts.js3 小时前
如何下载使用Highcharts Grid 开发web可编辑表格
前端·人工智能·grid 表格·web 表格·在线编辑表格·highcharts表格安装·表格开发工具
用户62960593247053 小时前
从“浏览器根本区分不了”到真正实现:刷新不退出,关闭标签页自动退出,前端判断浏览器关闭和刷新
前端
qetfw3 小时前
MWU:Vue 3 + FastAPI 的 MaaFramework 跨平台 WebUI 源码
前端·vue.js·python·fastapi·开源项目·效率工具
mfxcyh3 小时前
Vue3+Ant Design Vue 实现手动异步文件上传(含大小校验、弹窗上传)
前端·javascript·vue.js
wordbaby3 小时前
Web端热敏标签打印完整解决方案(QZ Tray + 译维A42)
前端
swipe3 小时前
03|Axios 请求进了后端之后:Controller、Request、Response 是怎么接住它的?
前端·后端·全栈
meilindehuzi_a4 小时前
Vue3进阶基础:指令修饰符、v-model表单绑定、动态样式、计算属性与侦听器实战
前端·javascript·vue.js
csdn2015_4 小时前
vue 前端运行命令
前端·javascript·vue.js