如何防止前端水印被破解

“ 最近看到一篇有趣的文章,是关于前端生成的水印如何防止用户破解,通常来说水印的生成应该是通过服务端来的,但是我们也可以了解一下如果让前端来实现,我们应该如何做?”

1. 前端和后端生成水印方法对比

前端方案

优点:可以减少服务端运算量和内存,并且能够快速响应请求;

缺点:安全性很差,容易被用户破解,窃取到没有水印的原图。

后端方案

优点:安全性很高,不容易被用户破解;

缺点:遇到大文件密集水印(复杂水印),会占用服务器内存和运算量,且请求时间会过长。

Web前端水印方案

2. 前端生成水印的方法

通过canvas画布进行绘画制作;

图片 + css中的background属性。

由于该片文章侧重点不是如何生成水印,所以针对于上面两种方法不做过多说明,如何想要了解可以网上搜索查看。

3. 如何防止水印被破解之方法一(粗暴)

由于水印是在前端实现的,那么针对于用户的操作使得被破解的风险很大,只要用户打开控制台,在你没有做相关防护措施下,那么你的水印就属于白做了。
就拿上面两种前端生成水印方法来说:

(1)如果是使用canvas画的水印,其实打开控制台之后可以找到对应的canvas元素直接删除改dom元素即可;
(2)如果是使用图片 + css属性制作的水印,找到对应的元素,直接修改其对应的属性即可。
所以其实前端做水印处理,风险是真的大!!!

解决:既然你可以打开控制台,那么我就阻止你打开控制台的行为(过于暴力图片),首先打开控制台有两种方式:
(1)键盘点击事件:F12;
(2)屏蔽浏览器右键默认事件。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useEffect } from "react";
import "./styles.css";

export default function App() {
useEffect(() => {
// 阻止F12键盘事件
document.addEventListener("keydown", (e) => {
console.log(1111);
if (e.keyCode === 123) return (e.returnValue = false);
});

// 屏蔽浏览器默认右键事件
document.addEventListener("contextmenu", (e) => {
console.log(2222);
return (e.returnValue = false);
});
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}

具体代码连接:
https://codesandbox.io/s/part-one-y53bs

针对于上面的解决方案还是太过于暴力了,鼠标右键之后不止检查元素一个功能,还有其他的功能,这样做的话只会影响用户体验感。

4. 如何防止水印被破解之方法二(取巧)

既然说到了打开控制台的过程,那么还可以考虑到一种取巧的方法。

首先说一下大致的思路:

console方法的执行是需要打开控制台才能执行,可以利用在console方法中打印Date值,在调用Date值后它会去调用toString方法,此时我们可以重写toString方法,换而言之toString方法被执行意味着控制台被打开了。
具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useEffect } from "react";
import "./styles.css";

export default function App() {
let num = 0;
const devtools = new Date();
devtools.toString = () => {
num++;
if (num > 1) {
// 在这里放入你的代码,比如我这里会让他跳到codepen
console.log("Console is opened");
window.location.href = "https://baidu.com";
return Date.prototype.toString.call(devtools); // 返回devtools结果(这一步不是必须的)
}
};

useEffect(() => {
console.log("devtools1111", devtools);
}, [devtools]);

return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}

具体代码连接:
https://codesandbox.io/s/part-six-0otm9

5. 如何防止水印被破解之方法三(比方案一好点)

方案一过于粗暴,我们再来看看方案二:
由于操作的是浏览器,那么可以考虑结合浏览器本身的一些属性来做处理,我们可以考虑检查浏览器可视区域(inner)和浏览器窗口区域(outer)的差值来判断用户是否打开了控制台。
核心实现代码原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useEffect } from "react";
import "./styles.css";

const threshold = 76;
export default function App() {
const observeSize = () => {
/**
* outerWidth: 整个浏览器窗口宽度,包括边栏
* innerWidth: 浏览器可视区域宽度(页面宽度)
* */
const width = window.outerWidth - window.innerWidth > threshold;
const height = window.outerHeight - window.innerHeight > threshold;
if (width || height) {
console.log("open devTools");
}
};

useEffect(() => {
window.addEventListener("resize", observeSize);
}, []);

return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}

具体代码连接:
https://codesandbox.io/s/part-two-feebp?file=/src/App.js

该方法也有弊端,由于是根据浏览器窗口区域和可视区域的差值来判断,此时我们可以新起一个窗口来打开控制台,该判断就不准确了,也可以利用这一点避免进入到代码判断中。
新起一个浏览器窗口
新起一个浏览器窗口

6. 如何防止水印被破解之方法四(重点)

在上部分中提到改变dom元素,那么我们可以考虑一下MutaitonObserver内建对象(DOM变动观察器)。
MutaitonObserver作用:观察DOM元素,并在检测到更改时触发回调。
MutaitonObserver语法:

1
2
3
4
5
6
7
8
9
10
11
// 首先,创建一个带有回调函数的观察器
let observer = new MutationObserver(callback);
// 然后将其附加到一个 DOM 节点
observer.observe(node, config);

// config 是一个具有布尔选项的对象,该布尔选项表示“将对哪些更改做出反应”
// childList --- node的直接子节点的更改
// subtree --- node的所有后代的更改
// attributes --- node的特性
// attributeFilter --- 特性名称数组,只观察选定的特性
// characterData --- 是否观察node.data(文本内容)

DOM 变动观察器(Mutation observer)

react中使用MutaitonObserver对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// https://www.jianshu.com/p/c0df31fbccca
import { useEffect, useState, useRef } from "react";
import "./styles.css";

export default function App() {
const ref = useRef();
const addWatch = (ele) => {
// 观察器的配置(需要观察什么变动)
let config = {
attributes: true, // node 的特性
childList: true, // node 的直接子节点的更改
subtree: true, // node 的所有后代的更改
characterData: true // 是否观察 node.data(文本内容)
};
const mutationCallback = (mutation) => {
// 操作页面上的dom元素后,返回的值是数组形式
console.log("mutation111", mutation);
};

let observer = new MutationObserver(mutationCallback);
observer.observe(ele, config);
};

useEffect(() => {
addWatch(ref.current);
}, []);

return (
<div className="App" ref={ref}>
<input />
<h2>Start editing to see some magic happen!</h2>
</div>
);
}

具体代码连接:
https://codesandbox.io/s/part-three-cf8l5?file=/src/App.js

在看到上面MutaitonObserver的使用之后,我们回过头再看水印中的使用就知道怎么做了。具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// https://www.jianshu.com/p/c0df31fbccca
import { useEffect, useState, useRef } from "react";
import "./styles.css";

let CONFIG = {
attributes: true, // node 的特性
childList: true, // node 的直接子节点的更改
subtree: true, // node 的所有后代的更改
characterData: true // 是否观察 node.data(文本内容)
};

export default function App() {
let observer = null;
const ref = useRef();
const reset = (expression = () => {}) => {
observer.disconnect();
// 执行恢复方法
expression();
observer.observe(ref.current, CONFIG);
};

const mutationCallback = (mutation) => {
const record = mutation[0];
console.log("record111", record);
// 在控制台修改元素的属性和样式
if (record.type === "attributes" && record.attributeName === "style") {
reset(() => {
console.log("modify attributes or style");
ref.current.setAttribute("style", "color: red");
});
} else if (record.type === "characterData") {
// 控制台修改元素文本内容
reset(() => {
ref.current.textContent = "dont modify";
console.log("modify characterData");
});
}
};

const addWatch = (ele) => {
// reset(ele);
console.log(22222);
observer = new MutationObserver(mutationCallback);
observer.observe(ele, CONFIG);
};

useEffect(() => {
addWatch(ref.current);
}, []);

return (
<div className="App" ref={ref}>
<input />
<h2 style={{ color: "red" }}>Start editing to see some magic happen!</h2>
</div>
);
}

具体代码连接:
https://codesandbox.io/s/part-five-7os9u?file=/src/App.js

ps:通常一般来说都是后端来处理,当然对于有心之人使用什么办法都会被破解的,只能说我们比之前要做的更好一些。

最后特别感谢这篇文章以及作者

前端如何防止用户修改水印?