产生这一文档的原因来自本人上家单位的产品里有明水印这一需求,但是查找到对应元素后水印可被移除,然后和咱们产品的水印相对比发现咱们明水印无法移除,感受上更为严谨,所以将仔细研究后的内容作出总结。
为什么要使用水印
- 对于公司机密性文件,员工有可阅读的权限,为防止员工泄漏机密而展示,方便订阅到操作泄漏的人
- 为标明作者,防止窃取他人努力结果而添加的
如何创建水印
元素绘制法
使用元素进行全局平铺的方式对DOM树上的文本节点,绘制压力等都有一定的影响,因此我们不建议这么进行,有兴趣的人可以自己搜搜看看
svg绘制法
和下方canvas绘制原理相似,这里不做过多描述
canvas绘制法
使用canvas绘制的好处
- canvas消耗GPU,对CPU消耗小,绘制快
- dom元素数量会少,减少对DOM树的渲染压力
- 使用css进行操作,速度快,效果好
绘制步骤
-
首先我们需要一个具体的画布进行绘制,这里的canvas必须存在在界面上,否则无法进行绘制,并在绘制结束生成链接地址
- 首先我们会通过定义旋转角度,名称,水印字体的大小而绘制生成以下的一个图片,此时的canvas元素在绘制时我们不期望它可见,在绘制初期需要将其移出界面
// 绘制生成水印图片 function createWaterMark({ name = "", angle = -20, fontSize = 16 }) { const txt = name; // canvas必须在界面存在才可进行绘制 const canvas = document.createElement("canvas"); document.body.appendChild(canvas); // 长度计算 const width = fontSize * txt.length; // 角度旋转计算, 这里不建议超过45度以上的旋转,所以取模 const angles = (Math.PI / 180) * (angle % 45); // 高度计算,这里用到三角函数计算,防止不同长度展示差异过大 const height = Math.ceil(Math.sqrt(Math.abs(Math.sin(angles)) * 100 * width)) + fontSize; // 因为绘制图片会旋转canvas图层,所以要计算旋转后不可见区域的长,增宽绘布 const discrepancy = Math.ceil(Math.sqrt(Math.abs(Math.sin(angles)) * 100 * height * 2)) + fontSize; // 定宽高 canvas.width = width * 2 + discrepancy; canvas.height = height * 2; // 写canvas样式移出界面,防止还未移除canvas时的闪屏 canvas.style.cssText = ` width: ${width * 2 + discrepancy}px; height: ${height * 2}px; position: fixed; bottom: -100%; `; // 绘制 const ctx = canvas.getContext("2d"); // 清理绘布 ctx.clearRect(0, 0, 180, 100); // 旋转 ctx.rotate(angles); ctx.beginPath(); ctx.fillStyle = "#000"; // 透明度 ctx.globalAlpha = 0.1; // 字体 ctx.font = `${fontSize}px serif`; ctx.fillText(txt, 0, height); ctx.stroke(); ctx.beginPath(); ctx.fillStyle = "#f00"; ctx.fillText(txt, width, height * 2); ctx.stroke(); const urls = canvas.toDataURL(); return urls; } ``` 2. 绘制完成后canvas元素不被我们需要,我们就可以执行 `canvas.remove();`的操作 - 首先我们会通过定义旋转角度,名称,水印字体的大小而绘制生成以下的一个图片,此时的canvas元素在绘制时我们不期望它可见,在绘制初期需要将其移出界面
-
创建元素平铺界面,生成水印效果
为了使水印可见且可以达到点击穿透的效果,我们需要在css里注意z-index的设置和pointer-events: none;的设置
// 创建放置水印的方式
function putWaterMark({ name = "", angle = -20, fontSize = 16 }) {
// 创建元素塞入界面,这里可将应用获取名称操作前置,所以没有拆分很细,否则建议可以内存canvasUrl的地址
const watermark = document.createElement("div");
watermark.className = "watermark";
// 背景图默认平铺
watermark.style.backgroundImage = `url(${createWaterMark({
name,
angle,
fontSize,
})})`;
document.body.appendChild(watermark);
}
// 绘制方式抽离,方便监听处调用
const initWaterMark = () => {
putWaterMark({
name: "猴霸天",
});
};
// 初识调用
initWaterMark();
3. 兜底不法的水印移除操作
这里主要为了一些懂一些代码的人直接删除水印元素而定,其余的特殊情况需要借助暗水印
// 监听元素改变,预防水印直接操作删除
// 创建一个 MutationObserver 对象
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.removedNodes.length > 0) {
// 当被删除元素为水印时重新执行填入操作
if (mutation.removedNodes[0].className === "watermark") {
initWaterMark();
}
}
});
});
// 选择要监听的目标元素
const targetElement = document.querySelector(".watermark");
const parentNode = targetElement.parentElement;
// 开始观察目标元素的子节点变化
observer.observe(parentNode, {
childList: true,
});
一些异常行为(别瞎用哈)
- 直接搜索删除watermark ==》 这里就是本篇上述的方式,用observe监听给水印在添加上去
- 复制body删除原始body
Chrome Devtools的Debuger禁用js,再操作删除watermark- 抓包更改原始代码取消监听
以上除了第一种以外,其他都是我们不可控的一些行为,这时候就要用到暗水印,一般分两种,前端绘制页面绘制和oss桶写入时绘制,感兴趣的可以自己搜搜,方法很多,对安全性来说更保险。