防篡改水印
在各类管理后台、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. 典型用法示例
// 创建观察器: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 中,watch 和 watchEffect 是最常用的两个侦听 API:
watch(source, callback, options):- 需要显式指定监听对象;
- 默认首次不执行,可通过
immediate: true控制; - 能获取新旧值,适合需要做差异比较的场景。
watchEffect(effect):- 不需要显式 source,会自动收集 effect 中读取到的响应式依赖;
- 首次一定会执行一次,之后在依赖变化时再次执行;
- 适合「依赖变了就整体重算」的场景。
简单示例:
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 的核心实现逻辑,演示如何结合 watchEffect 与 MutationObserver 来实现防篡改水印:
// 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仅监听如style、class等必要属性,减少无关噪音。
- 对父容器使用
- 避免死循环:
- 重建水印本身也是一次 DOM 变更;
- 回调中只在「确认是水印节点相关变更」时才修改
uId,并尽量在检测到后立即return,避免同一批 records 内多次自增导致频繁重建。
- 性能权衡:
- 对于大规模复杂 DOM 树,应避免把观察范围设置在
document.body级别; - 更推荐为特定业务容器创建局部水印和局部监控。
- 对于大规模复杂 DOM 树,应避免把观察范围设置在
- 安全边界认知:
- 这种前端防篡改方案可以有效阻挡「随手删 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 / watchEffect) | https://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 |
相关文章
图片上传前的自定义水印实践
项目需要在图片上传前叠加动态水印。水印内容不是固定文案,而是由多个动态元素组成(如时间、地点、业务字段等)。
GridView 宫格加载渲染优化
系统首页 GridView 宫格模块接口耗时不高,但首次进入总耗时接近 22s。复盘耗时分层定位过程、代码层面的瓶颈(约 2000 行、100 个 tab 重复节点)、优化手段与最终指标对比。
老系统升级与兼容改造实践
公司原有业务系统采用 Node + jQuery 架构,长期运行后暴露出典型遗留系统问题:
新商家系统性能优化实践
商家系统新版本上线后,团队持续针对构建效率与用户体验做了一轮系统性优化。本文记录核心方案与落地结果,供后续版本复用。
键盘弹起导致底部被顶起问题(H5 适配)
在移动端 H5 页面中,当用户聚焦 input、textarea 等可编辑元素、软键盘弹出时,常见表现包括:
企业微信 uni-app H5:OAuth 回退白屏与列表缓存
第三方 BI 报表项目,企业微信内嵌 H5,uni-app 编译,history 模式。主页面 BiLink 同时承担静默授权和列表展示,上线后碰到两个问题:授权完清掉 URL 参数,iOS 侧滑返回还是白屏;列表加了缓存以后,刷新行…
Series
team documents
2 / 22