“ 最近看到一篇有趣的文章,是关于前端生成的水印如何防止用户破解,通常来说水印的生成应该是通过服务端来的,但是我们也可以了解一下如果让前端来实现,我们应该如何做?”
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:通常一般来说都是后端来处理,当然对于有心之人使用什么办法都会被破解的,只能说我们比之前要做的更好一些。
最后特别感谢这篇文章以及作者
前端如何防止用户修改水印?