FORMA

防篡改水印

在各类管理后台、SaaS 平台或内部系统中,页面水印已经是非常常见的能力:一方面在截图时携带账号、姓名、时间等信息,降低截图外传的风险;另一方面在上传图片或文档预览时,通过水印标记上传者与时间,满足审计和合规需求。

从实现难度看,水印本身并不复杂;但从安全视角看,如果前端水印可以被轻易删除或隐藏,其安全价值会大幅打折。本文结合浏览器原生 MutationObserver 与 Vue 3 的响应式机制,介绍一种「被删除 / 被改动后自动恢复」的防篡改水印实现思路。

水印的安全?

水印一般是为了保证数据的安全性。例如,有这样一个典型场景:

  • 企业账户登录某平台系统,每个核心页面都需要叠加水印;
  • 水印内容包含登录人姓名与时间,如:张三 2023-05-27
  • 当用户截图用于反馈问题或沟通时,水印可以帮助追溯截图来源,从而降低泄露风险。

但如果水印只是一个普通 DOM 元素,部分开发者可以在浏览器 DevTools 中定位到水印节点,直接删除或改成 display: none,使得页面在本次会话中一直处于「无水印」状态——这会显著降低水印的安全性。

因此,「防篡改水印」的目标是:

  • 即便水印节点被删除 / 被修改,也能在极短时间内自动恢复
  • 同时尽量避免为页面引入明显的性能与稳定性问题。

提高安全:依赖 MutationObserver 的 DOM 监听

1. MutationObserver 是什么?

MutationObserver 是浏览器原生提供的、用于监听 DOM 树变化的接口,是早期同步 Mutation Events 的替代方案。

MDN:MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。参见 MutationObserver - Web APIs | MDN

在防篡改水印场景下,我们最关心:

  • childList:水印节点是否被删除 / 替换;
  • attributes:水印节点的样式、class 等是否被恶意修改;
  • subtree:是否对子树做了批量变更(如整个水印容器被清空)。

2. 典型用法示例

javascript
// 创建观察器:records 是本轮收集到的所有 DOM 变更
const observer = new MutationObserver((records) => {
  for (const record of records) {
    if (record.type === "childList") {
      console.log("子节点发生了增删");
    }
    if (record.type === "attributes") {
      console.log("属性被修改:", record.attributeName);
    }
  }
});

// 配置并开始观察
observer.observe(targetNode, {
  attributes: true,
  childList: true,
  subtree: true,
});

// 之后如有需要,可停止观察
observer.disconnect();

相较于早期同步事件,MutationObserver异步批量触发的:浏览器会先收集一批 DOM 变更,再统一调用回调,从而减少性能压力。

Vue 3 中的依赖监听:watch 与 watchEffect

为了让「被篡改 → 触发重建」这条链路在代码层更清晰,我们通常会引入一个简单的「触发标记」:例如计数器 uId,每当检测到水印被动手脚,就让它自增一次。然后使用 Vue 3 的侦听能力,在 uId 变化时重新生成水印。

在 Vue 3 中,watchwatchEffect 是最常用的两个侦听 API:

  • watch(source, callback, options)
    • 需要显式指定监听对象;
    • 默认首次不执行,可通过 immediate: true 控制;
    • 能获取新旧值,适合需要做差异比较的场景。
  • watchEffect(effect)
    • 不需要显式 source,会自动收集 effect 中读取到的响应式依赖;
    • 首次一定会执行一次,之后在依赖变化时再次执行;
    • 适合「依赖变了就整体重算」的场景。

简单示例:

javascript
const count = ref(0);

// 模拟自增
const addCount = () => count.value++;

watch(
  () => count.value,
  (val, oldVal) => {
    // val: 最新值;oldVal: 上一次的值
  },
  {
    immediate: true,
    deep: true,
  }
);

watchEffect(() => {
  // 访问 count 时会被自动收集为依赖
  console.log("当前 count:", count.value);
});

在水印组件中,我们可以借助 watchEffect 来:

  • 把「水印配置」(文字、颜色、透明度等)和「重建水印 DOM」的逻辑绑在一起;
  • 只要依赖变化(包括 uId 自增),就重新创建水印节点。

下图是整体逻辑示意:

水印逻辑

综合实现:防篡改水印核心代码

下面是一段基于 Vue 3 的核心实现逻辑,演示如何结合 watchEffectMutationObserver 来实现防篡改水印:

javascript
// parentRef:水印父容器(一般为页面主容器或某个 section)
const parentRef = ref();

// uId:触发重建水印的计数器
const uId = ref(0);

// useWater:内部基于 canvas 生成水印图案,返回 { base, styleSize }
const bg = useWater(props);
const { base, styleSize } = bg.value;

let div; // 当前水印节点

// 依赖驱动的水印重建
watchEffect(() => {
  // 访问 uId,建立依赖关系
  uId.value;
  if (!parentRef.value) return;

  // 移除旧水印
  if (div) {
    div.remove();
  }

  // 创建新水印容器
  div = document.createElement("div");
  div.style.backgroundImage = `url(${base})`;
  div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
  div.style.backgroundRepeat = "repeat";
  div.style.width = "100%";
  div.style.height = "100%";
  div.style.position = "absolute";
  div.style.inset = "0";

  parentRef.value.appendChild(div);
});

// 初始化观察器
let ob;

onMounted(() => {
  ob = new MutationObserver((records) => {
    for (const record of records) {
      // 1. 节点被删除
      for (const dom of record.removedNodes) {
        if (dom === div) {
          uId.value++;
          return;
        }
      }

      // 2. 节点属性被修改(例如被设置 display:none)
      if (record.target === div) {
        uId.value++;
        return;
      }
    }
  });

  ob.observe(parentRef.value, {
    childList: true, // 监听子节点新增/删除
    attributes: true, // 监听属性变更
    subtree: true, // 监听子树
  });
});

// 组件卸载时清理,防止内存泄漏
onUnmounted(() => {
  if (ob) {
    ob.disconnect();
  }
  div = null;
});

可以总结为:

MutationObserver 负责发现「水印被篡改」,uId 负责发出「需要重建」的信号,watchEffect 则负责真正重建水印。

useWater 的内部实现通常基于 <canvas>:绘制水印文字、角度、透明度等,再通过 canvas.toDataURL 生成 base64 图片,本文不展开,可按项目实际需求设计。

设计细节与注意事项

  • 监听范围要适度
    • 对父容器使用 subtree: true 虽然方便,但也会带来更多变更记录;
    • 可以结合 attributeFilter 仅监听如 styleclass 等必要属性,减少无关噪音。
  • 避免死循环
    • 重建水印本身也是一次 DOM 变更;
    • 回调中只在「确认是水印节点相关变更」时才修改 uId,并尽量在检测到后立即 return,避免同一批 records 内多次自增导致频繁重建。
  • 性能权衡
    • 对于大规模复杂 DOM 树,应避免把观察范围设置在 document.body 级别;
    • 更推荐为特定业务容器创建局部水印和局部监控。
  • 安全边界认知
    • 这种前端防篡改方案可以有效阻挡「随手删 DOM」这类轻度攻击;
    • 但对于刻意对抗(例如关闭 JS、注入脚本、修改浏览器环境等),仍然需要配合后端审计、权限控制、操作日志等手段综合防护。

参考文献

资料链接
MDN:MutationObserver(英文)https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
MDN:MutationObserver(中文)https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
Vue 3 官方文档:侦听器(watch / watchEffecthttps://cn.vuejs.org/api/reactivity-core.html#watch
Vue 3 官方文档:响应性基础https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html
防篡改水印组件实现示例(思路参考)https://www.wsisp.com/helps/61733.html