**目的 :**从概念,业务场景上理解水印,作为前端开发者能快速提出水印方案并实现,熟悉实现原理及相关技术;
什么是水印?
使用水印解决什么问题?
- 为了防止信息泄露或知识产权被侵犯,在web的世界里,对于图片文档等增加水印处理是十分有必要的。
- 说人话:****对于某些机密文档或者内部文件,当文档外流的时,可以通过水印追究到责任人;
应用的业务场景?
标记原创或来源
一个资源绑定一个固定用户,一般创建时处理一次,标记原创或来源,如掘金;
如下,为掘金水印:
标记查看者
一个资源关联多个不同用户,每个用户查看时都要处理,标记当前查看者,如飞书;
常见水印分类 & 特点
按添加环境分类:前端水印(浏览器环境添加),后端水印(服务环境添加)
从实现方式分类:显性水印,数字水印
前端方案
-
不占用服务器运算量、内存,能够快速响应请求
-
安全性较低,有心人容易通过各种骚操作获取到源文件
后端方案
-
安全性较高,无法获取到源文件
-
当遇到大文件密集水印,或是复杂水印,占用服务器内存、运算量,请求时间过长
显性水印
-
容易处理,算法较为简单
-
攻击者就可以通过裁剪、模糊等操作对水印进行攻击消除,同时显性水印也会破坏图片的完整性
数字水印
-
算法一般较为复杂
-
抗攻击能力较强
实现方案
显性水印 + Canvas + 保护程序
实现:定时器监听或MutationObserver,提醒用户操作违法,并且删除掉水印,并且重新生成水印
攻击:完全可以拷贝下来整个已经渲染完成的HTMLandCSS,去除JS的保护干扰,故而,此方案只防住了君子和小白
显性水印 + Canvas + base64
- 给现有图片打水印:
用canvas去绘制图片,加上水印文字,然后转成base64,展示在页面中,使html中的img资源是加过水印的base64;
- 上传图片打水印:
文件选择后,拿到文件对象,把文件转成base64放到img标签中预览,点击确定上传时,将图片用canvas绘制下来,绘制水印,把canvas转成base64,再转成二进制文档上传;
如何选择方案?
没有最好的方案,只有根据环境与需求,使用当前最适合的方案,安全性、性能、实现成本之间权衡利弊。
实战(显性水印 + Canvas + 保护程序)
开始之前需要弄清楚的几个问题:
水印放在哪里?
盖在内容最上层的(z-index:999),并且要能穿透事件(pointer-events: none; 元素永远不会成为鼠标事件的target)
什么是Canvas?
一句话解释,一个可以使用 JavaScript 等脚本语言向其中绘制图像的 HTML 标签,应用于图表,动画,游戏等;canvas是画布,js是画笔;
Canvas常用API
// 建立并返回一个二维渲染上下文,contextType 为 webgl 时,可以建立一个三维上下文
canvasElement.getContext("2d")
// 状态相关
ctx.save(); // 存储当前状态
ctx.restore(); // 恢复到最近save的状态
// 绘制矩形
ctx.clearRect(x,y,w,h); // 以x,y为起点,绘制宽w,高h的的透明矩形
ctx.fillRect(); // 以x,y为起点,绘制宽w,高h的的实心矩形
ctx.strokeRect(); // 以x,y为起点,绘制宽w,高h的的描边矩形
// 绘制图片
ctx.drawImage(image,x,y,w,h); // image 图片元素,x,y为起点绘制
// 绘制文本相关
ctx.fillText(this.text, 10, 0);// 在(x,y)位置绘制(填充)文本
ctx.strokeText(this.text, 10, 0); // 在(x,y)位置绘制(描边)文本--空心文本
// 字体设置
ctx.font = "normal " + this.fontSize + "px Microsoft Yahei";
// 填充描边样式
ctx.fillStyle = this.color; // 设置填充颜色
ctx.strokeStyle = this.color; // 设置描边颜色
// 变换相关
ctx.translate(boxWidth * i, j * boxHeight - top); // 平移
ctx.rotate((-30 * Math.PI) / 180); // 旋转,用弧度
// 180 度(degree) = 3.141593 弧度(radian)
ctx.scale(x,y) // x,y 宽高缩放
所谓的保护程序是啥?
监听dom变化,而重新生成水印;
MutationObserver是什么?
一个web API 接口,提供了监视对DOM树更改的能力;
代码实现参考:
import { Component, Vue, Prop, Watch } from "vue-property-decorator";
import localStorage from "@/utils/localStorage";
@Component({})
export default class Watermark extends Vue {
@Prop() content;
id = "watermark-canvas";
color = "#000";
fontSize = 20;
width = window.screen.width;
height = window.screen.height;
ccsText = "position: fixed; z-index: 999; pointer-events: none; opacity: 0.1;";
text = "水印文案";
observer = null;
clearCanvas() {
const oldCanvas = document.getElementById(this.id);
if (oldCanvas) {
oldCanvas.parentNode.removeChild(oldCanvas);
}
}
createCanvas() {
//创建画布
const body = document.getElementsByTagName("body");
const canvas = document.createElement("canvas");
const id = this.id;
//设置画布id
canvas.setAttribute("id", id);
canvas.width = this.width;
canvas.height = this.height;
canvas.style.cssText = this.ccsText;
body[0].appendChild(canvas);
}
draw() {
const canvasElement = document.getElementById(this.id);
const ctx = canvasElement.getContext("2d");
const { width: textWidth } = ctx.measureText(this.text);
const boxHeight = 180;
const boxWidth = textWidth + 200;
const yCount = parseInt(this.height / boxHeight); // 行数
const xCount = parseInt(this.width / boxWidth) + 1; // 最多产生多少个
const top = 50;
for (let i = 0; i < xCount; i++) {
for (let j = 0; j < yCount; j++) {
ctx.save();
ctx.translate(boxWidth * i, j * boxHeight - top);
ctx.rotate((-30 * Math.PI) / 180);// 角度转换 2PI = 360度
ctx.fillStyle = this.color;
ctx.font = "normal " + this.fontSize + "px Microsoft Yahei";
ctx.fillText(this.text, 10, 0);
ctx.restore();
}
}
}
init() {
this.clearCanvas();
this.clearLock();
this.createCanvas();
this.draw();
this.safetyLock();
}
created() {
this.text && this.init();
}
safetyLock() {
// 选择需要观察变动的节点
const targetNode = document.getElementById(this.id);
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
console.log("A child node has been added or removed.");
this.init();
} else if (mutation.type === "attributes") {
console.log(
"The " + mutation.attributeName + " attribute was modified."
);
this.init();
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
this.observer = observer;
}
clearLock() {
if (this.observer) {
this.observer.disconnect();
}
}
destroyed() {
this.clearCanvas();
this.clearLock();
}
}
思考
你能在画布上画出这样的图吗?
更多
看看飞书水印实现
数字水印见下面文档: