使用Vue實(shí)現(xiàn)防篡改的水印
我們?cè)谄綍r(shí)上網(wǎng)的時(shí)候會(huì)看到有些圖片是加水印的:

像這種加水印的操作往往是后端來做的,不過有些站點(diǎn)要保護(hù)的知識(shí)產(chǎn)權(quán)類型比較多,不光是圖片,可能還有視頻或者文字。
對(duì)不同類型的東西去加這個(gè)水印,后端操作起來就可能比較麻煩,因?yàn)樗∵@個(gè)東西防君子不防小人,他要搞你的話始終能搞你。
所以我們水印的作用,就是給他做一個(gè)適當(dāng)?shù)南拗?,讓他沒有那么輕易的能搞到。
因此現(xiàn)在有些站點(diǎn)開始逐步的讓前端來制作這個(gè)水印了。
如果你是用的是 React 來開發(fā)的話就比較簡(jiǎn)單了:

這個(gè) Ant Design 這個(gè)庫(kù),它本身就有一個(gè)組件叫做 Watermark 水印組件,通過這個(gè)組件就可以給一個(gè)區(qū)域加上一個(gè)水印,非常的 so easy 開發(fā)成本極低,無論這個(gè)區(qū)域是圖片還是文字或者視頻,都無所謂。
但是如果你使用的是 Vue 來開發(fā)的話很遺憾,無論是 Element UI 還是 Ant Design Vue,都沒有這個(gè) Watermark 組件。
那么就需要我們自己手動(dòng)的去編寫,其實(shí)編寫這個(gè)組件也并不復(fù)雜,主要是要考慮兩個(gè)問題:
- 如何來生成水印
- 如何來防止篡改
如何生成水印
我們先來看第一步如何生成水印。
基本思路與準(zhǔn)備
我們可以有這么一個(gè)思路:

比如我們要在上圖的區(qū)域做水印,那么就在區(qū)域里加上一個(gè) div,div 填充滿整個(gè)區(qū)域,然后給這個(gè) div 一張水印的背景圖,然后讓背景圖重復(fù)就可以了。
這個(gè)背景圖我們可以使用 canvas 來畫。
所以基于這么一個(gè)思路,我們就可以寫出這么一個(gè)代碼結(jié)構(gòu):

我們引入封裝的 Watermark 組件,里邊傳入任何內(nèi)容,可以是文字也可以是視頻,然后就給這個(gè)區(qū)域加上水印。
通過 text 傳入水印的文本。
那么我們看看組件里咋寫的:
<template>
<div class="watermark-container">
<slot></slot>
<!-- 我們要做的就是在這里添加一個(gè) div,填充滿整個(gè)區(qū)域,設(shè)置水印背景并且重復(fù) -->
</div>
</template>
<script setup>
import useWatermarkBg from './useWatermarkBg';
// 定義一些基本的屬性( 如果說你想開發(fā)的更加完善,可以加入更多的屬性來適應(yīng)你的要求 )
const props = defineProps({
text: { // 傳入水印的文本
type: String,
required: true,
default: 'watermark',
},
fontSize: { // 字體的大小
type: Number,
default: 40,
},
gap: { // 水印重復(fù)的間隔
type: Number,
default: 20,
},
});
// useWatermarkBg 函數(shù)用來創(chuàng)建一個(gè) canvas 圖片
// 將屬性傳遞進(jìn)去就返回個(gè)創(chuàng)建好的對(duì)象
const bg = useWatermarkBg(props);
console.log('bg.value >>> ', bg.value)
</script>目前組件的代碼還是比較簡(jiǎn)單,我們看一下 useWatermarkBg 返回的數(shù)據(jù)是什么:

這里打印了兩個(gè)對(duì)象,是因?yàn)槲覀冇袃蓚€(gè)水印區(qū)域,這個(gè)對(duì)象里有三個(gè)屬性:
base64:表示 canvas 生成圖片的 dataurl,到時(shí)候就可以用它來做背景 size:表示 canvas 的寬高 styleSize:表示 canvas 的 DPR,如果想要用非常清晰的尺寸的話就用這個(gè),這個(gè)值和 window 的devicePixelRatio 有關(guān),如果你不知道的話可以關(guān)注子辰,后期會(huì)更新相關(guān)的文章 。
那么我們看看 useWatermarkBg 函數(shù)是怎么寫的,代碼也很簡(jiǎn)單:
import { computed } from 'vue';
export default function useWatermarkBg (props) {
return computed(() => {
// 創(chuàng)建一個(gè) canvas
const canvas = document.createElement('canvas');
const devicePixelRatio = window.devicePixelRatio || 1;
// 設(shè)置字體大小
const fontSize = props.fontSize * devicePixelRatio;
const font = fontSize + 'px serif';
const ctx = canvas.getContext('2d');
// 獲取文字寬度
ctx.font = font;
const { width } = ctx.measureText(props.text);
const canvasSize = Math.max(100, width) + props.gap * devicePixelRatio;
canvas.width = canvasSize;
canvas.height = canvasSize;
ctx.translate(canvas.width / 2, canvas.height / 2);
// 旋轉(zhuǎn) 45 度讓文字變傾斜
ctx.rotate((Math.PI / 180) * -45);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.font = font;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 將文字畫出來
ctx.fillText(props.text, 0, 0);
return {
base64: canvas.toDataURL(),
size: canvasSize,
styleSize: canvasSize / devicePixelRatio,
};
});
}現(xiàn)在基本的數(shù)據(jù)有了,我們就要生成一個(gè)水印的背景的 div,填充在合適的位置。
生成水印填充背景
<template>
<div class="watermark-container">
<slot></slot>
<!-- 我們要做的就是在這里添加一個(gè) div,填充滿整個(gè)區(qū)域,設(shè)置水印背景并且重復(fù) -->
</div>
</template>
<script setup>
import useWatermarkBg from './useWatermarkBg';
const props = defineProps({
// ...
});
const bg = useWatermarkBg(props);
// 創(chuàng)建一個(gè) div
const div = document.createElement('div');
</script>我們這里使用 document.createElement 生成一個(gè) div,有同學(xué)可能會(huì)問,為什么不直接在填充的位置寫一個(gè) div 呢?因?yàn)椴恍?,至于為什么不行看到后邊就知道了,在最后進(jìn)行解釋,現(xiàn)在就使用 dom 來創(chuàng)建這個(gè) div。
現(xiàn)在呢我們給這個(gè) div 設(shè)置一些樣式:
<script setup>
import useWatermarkBg from './useWatermarkBg';
const props = defineProps({
// ...
});
const bg = useWatermarkBg(props);
const div = document.createElement('div');
// 獲取到解構(gòu)的值
const { base64, styleSize } = bg;
// 背景設(shè)置為 base64 的圖片
div.style.backgroundImage = `url(${base64})`;
// 背景的大小設(shè)置為 styleSize
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
// 重復(fù)方式設(shè)置為 repeat
div.style.backgroundRepeat = 'repeat';
// 設(shè)置子元素與父元素四個(gè)方向的間隔(這里設(shè)置為 0 的效果同寬高設(shè)置 100%)
div.style.inset = 0;
// z-index 設(shè)置為 9999 覆蓋上去
div.style.zIndex = 9999;
</script>樣式我們也只能通過上面的方式來添加,而不能直接寫成 class,具體原因后邊會(huì)解釋。
接下來我們要把這個(gè) div 添加到父元素里邊去:
<template>
<!-- 在父元素上添加 ref -->
<div class="watermark-container" ref="parentRef">
<slot></slot>
<!-- 添加一個(gè)div,填充滿整個(gè)區(qū)域,設(shè)置水印背景,重復(fù) -->
</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue';
import useWatermarkBg from './useWatermarkBg';
const props = defineProps({
// ....
});
// 聲明一個(gè) ref 并添加到父元素上
const parentRef = ref(null);
const bg = useWatermarkBg(props);
// watchEffect 中判斷是否可以獲取到父組件的 ref
watchEffect(() => {
// 獲取不到,就說明還沒有掛載,先出去
if (!parentRef.value) {
return;
}
// 獲取到則添加到父元素中
const { base64, styleSize } = bg;
const div = document.createElement('div');
div.style.backgroundImage = `url(${base64})`;
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
div.style.backgroundRepeat = 'repeat';
div.style.inset = 0;
div.style.zIndex = 9999;
// 然后將 div 加到父元素里
parentRef.value.appendChild(div);
});
</script>你可以會(huì)發(fā)現(xiàn)我們這里使用的是 watchEffect 來判斷是否能獲取到父元素,而不是在 onMounted 里邊,這是因?yàn)檫@一塊會(huì)涉及到后邊的防篡改,我們一會(huì)就知道了,現(xiàn)在暫且不用管就放在這里。

可以看到 div 已經(jīng)被添加進(jìn)去了,背景圖以及屬性都是有的,只不過這個(gè) div 不是絕對(duì)定位,要填充滿的話就得設(shè)置絕對(duì)定位:
<script setup>
// etc...
watchEffect(() => {
if (!parentRef.value) {
return;
}
const div = document.createElement('div');
const { base64, styleSize } = bg.value;
div.style.backgroundImage = `url(${base64})`;
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
div.style.backgroundRepeat = 'repeat';
div.style.inset = 0;
div.style.zIndex = 9999;
// 設(shè)置絕對(duì)定位
div.style.position = 'absolute';
// 設(shè)置點(diǎn)擊穿漏,防止底部元素失去鼠標(biāo)事件的交互
div.style.pointerEvents = 'none';
parentRef.value.appendChild(div);
});
</script>
你看,現(xiàn)在這個(gè)水印就加上了,沒有什么問題,那么第一步加水印就完成了。
接下來我們就要說第二步了,如何防篡改。
如何防篡改
用戶會(huì)怎么來篡改我們的水印呢?他有很多辦法,直接在頁(yè)面上操作不太可能,他主要的辦法就是進(jìn)入這個(gè)瀏覽器調(diào)試工具,找到我們這個(gè)水印的 div 然后刪除:

這樣一刪除就沒了,所以我們僅僅是把這個(gè)水印生成出來毫無意義,因?yàn)榭梢暂p松的刪除。
那這就要求我們必須要找到某一種方式,能夠監(jiān)控用戶對(duì)我們水印元素的操作,比如說刪除。
所以這個(gè)防篡改就涉及到兩件事:
- 如何監(jiān)控
- 重新生成
這就解釋清楚了為什么不直接在父元素里寫 div 的原因,因?yàn)橹苯釉诟冈乩飳懙脑捜绻麆h除掉的話無法重新生成,但是通過 document 添加的話就可以。
把 div 放在 watchEffect 里邊只要監(jiān)控到用戶動(dòng)了水印,只要在執(zhí)行一遍 watchEffect 就能重新生成一個(gè)新的水印添加進(jìn)去。
如果說我們不是在 watchEffect 里還是在 onMounted 里就沒辦法那做到重新運(yùn)行了。
同時(shí)也解釋了為什么樣式不能寫在 class 里,因?yàn)樵?calss 里的話,用戶通過調(diào)試工具更改的話,我們同樣無法監(jiān)控到。
好了,剛才的三個(gè)疑問現(xiàn)在都解決了。
如何監(jiān)控
現(xiàn)在的問題就是我們?nèi)绾稳ケO(jiān)控的問題了,我們?cè)趺粗烙脩魟?dòng)了水印呢?
那么這里就要說到一個(gè) API 了,叫做 MutationObserve 它可以監(jiān)控一個(gè)元素的變化,不僅可以監(jiān)控元素本事,還可以監(jiān)控元素里邊所有的子元素,無論是改動(dòng)元素的屬性,還是元素的內(nèi)容,這個(gè) API 都可以收到通知。
我們現(xiàn)在就利用這會(huì) API 來實(shí)現(xiàn)監(jiān)控,首先我們要搞清楚的是,到底要監(jiān)控誰(shuí),我們要監(jiān)控的不是水印的 div,而是整個(gè)組件,這樣就可以監(jiān)控到所有的東西了。
所以我們可以這樣寫:
<script setup>
import { ref, watchEffect, onMounted, onUnmounted } from 'vue';
// etc...
let ob;
onMounted(() => {
// 在 onMounted 里邊創(chuàng)建一個(gè) MutationObserver 來進(jìn)行監(jiān)控
// 一旦某個(gè)東西有變化就會(huì)運(yùn)行這個(gè)回調(diào)函數(shù)
ob = new MutationObserver((records) => {
// 并把變化記錄下來傳遞給我們
console.log('records >>> ', records)
});
// 創(chuàng)建好監(jiān)聽器之后,告訴監(jiān)聽器需要監(jiān)聽的元素
ob.observe(parentRef.value, {
// 監(jiān)聽的時(shí)候需要加一些配置
childList: true, // 元素內(nèi)容有沒有發(fā)生變化
attributes: true, // 元素本身的屬性有沒有發(fā)生變化
subtree: true, // 告訴它監(jiān)控的是整個(gè)子樹,就是包含整個(gè)子元素
});
});
// 在組件卸載的時(shí)候取消監(jiān)聽
onUnmounted(() => {
ob && ob.disconnect(); // 取消監(jiān)聽
});
</script>現(xiàn)在我們就基本設(shè)置好了,看一下效果如何:

在最開始的時(shí)候就打印了兩次,因?yàn)槲覀兲砑恿藘纱嗡〉?div,加這個(gè) div 的動(dòng)作就被監(jiān)聽到了。
返回值是一個(gè)數(shù)組,表示我們的操作動(dòng)作,動(dòng)作里邊也明確的表示是添加節(jié)點(diǎn),并且是 div 節(jié)點(diǎn)。

如果我們刪除水印的 div,同樣也觸發(fā)了我們的回調(diào)函數(shù),動(dòng)作也記錄到了我們刪除了一個(gè) div 的節(jié)點(diǎn)。
通過對(duì)動(dòng)作的了解我們就可以知道如何來監(jiān)控節(jié)點(diǎn)的刪除,獲取到刪除的節(jié)點(diǎn)并且與我們添加的節(jié)點(diǎn)對(duì)比,就知道用戶是否刪除了我們的水印節(jié)點(diǎn),我們就可以這樣來寫:
<script setup>
// 將 div 保存在外部因?yàn)橐袛喙?jié)點(diǎn)時(shí)使用
let div;
watchEffect(() => {
if (!parentRef.value) {
return;
}
// 判斷之前的節(jié)點(diǎn)是否有內(nèi)容,如果有的話刪除
if (div) {
div.remove();
}
const { base64, styleSize } = bg.value;
div = document.createElement('div');
div.style.backgroundImage = `url(${base64})`;
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
div.style.backgroundRepeat = 'repeat';
div.style.inset = 0;
div.style.zIndex = 9999;
div.style.position = 'absolute';
div.style.pointerEvents = 'none';
parentRef.value.appendChild(div);
});
let ob;
onMounted(() => {
ob = new MutationObserver((records) => {
// 循環(huán)節(jié)點(diǎn)的動(dòng)作
for (const record of records) {
// 如果有節(jié)點(diǎn)被刪除,循環(huán)一下判斷是否有水印的節(jié)點(diǎn)
for (const dom of record.removedNodes) {
if (dom === div) {
console.log('水印被刪除')
// ...
return;
}
}
// 如果有節(jié)點(diǎn)被修改,判斷一下是否是水印的節(jié)點(diǎn)
if (record.target === div) {
console.log('屬性被修改')
// ...
return;
}
}
});
ob.observe(parentRef.value, {
childList: true,
attributes: true,
subtree: true,
});
});
// 在組件卸載的時(shí)候取消監(jiān)聽
onUnmounted(() => {
ob && ob.disconnect(); // 取消監(jiān)聽
div = null; // 因?yàn)?div 是全局變量在寫在的時(shí)候值為空
});
</script>
水印刪除后事件就被觸發(fā)了。

屬性被修改時(shí)同樣會(huì)觸發(fā)事件。
重新生成
那么我們能監(jiān)控到事件了如何重新運(yùn)行 watchEffect 呢?因?yàn)?watchEffect 是收集依賴的,只要依賴變化了它就會(huì)重新運(yùn)行,所以我們可以手動(dòng)搞一個(gè)依賴:
<template>
<div class="watermark-container" ref="parentRef">
<slot></slot>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watchEffect } from 'vue';
import useWatermarkBg from './useWatermarkBg';
const props = defineProps({
text: {
type: String,
required: true,
default: 'watermark',
},
fontSize: {
type: Number,
default: 40,
},
gap: {
type: Number,
default: 20,
},
});
const bg = useWatermarkBg(props);
const parentRef = ref(null);
const flag = ref(0); // 聲明一個(gè)依賴
let div;
watchEffect(() => {
flag.value; // 將依賴放在 watchEffect 里
if (!parentRef.value) {
return;
}
if (div) {
div.remove();
}
const { base64, styleSize } = bg.value;
div = document.createElement('div');
div.style.backgroundImage = `url(${base64})`;
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
div.style.backgroundRepeat = 'repeat';
div.style.zIndex = 9999;
div.style.position = 'absolute';
div.style.inset = 0;
parentRef.value.appendChild(div);
});
let ob;
onMounted(() => {
ob = new MutationObserver((records) => {
for (const record of records) {
for (const dom of record.removedNodes) {
if (dom === div) {
flag.value++; // 刪除節(jié)點(diǎn)的時(shí)候更新依賴
return;
}
}
if (record.target === div) {
flag.value++; // 修改屬性的時(shí)候更新依賴
return;
}
}
});
ob.observe(parentRef.value, {
childList: true,
attributes: true,
subtree: true,
});
});
onUnmounted(() => {
ob && ob.disconnect();
div = null;
});
</script>這樣就可以完成了,只要監(jiān)控到刪除或者修改屬性,就會(huì)重新運(yùn)行 watchEffect 重新生成一個(gè)新的水?。?/p>

總結(jié)
水印是一種保護(hù)知識(shí)產(chǎn)權(quán)的手段,但是如果只是簡(jiǎn)單的生成水印,很容易被用戶篡改或刪除。
所以我們需要使用一些技巧來防止水印被破壞,比如使用 canvas 生成背景圖,使用 document.createElement 添加水印元素,使用 MutationObserver 監(jiān)控元素變化,使用 watchEffect 重新生成水印等。
這樣我們就可以實(shí)現(xiàn)一個(gè)比較安全的水印組件,提高我們的網(wǎng)站的安全性和可信度。
像 Ant Design 里邊的水印就是這樣做的,沿著這個(gè)思路我們就可以一步一步的把這個(gè)組件給它完善掉。
以上就是使用Vue實(shí)現(xiàn)防篡改的水印的詳細(xì)內(nèi)容,更多關(guān)于Vue實(shí)現(xiàn)防篡改水印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue中v-for循環(huán)給標(biāo)簽屬性賦值的方法
這篇文章主要介紹了vue中v-for循環(huán)給標(biāo)簽屬性賦值的方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-10-10
vue修改對(duì)象的屬性值后頁(yè)面不重新渲染的實(shí)例
今天小編就為大家分享一篇vue修改對(duì)象的屬性值后頁(yè)面不重新渲染的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08
VUE+elementui組件在table-cell單元格中繪制微型echarts圖
這篇文章主要介紹了VUE+elementui組件在table-cell單元格中繪制微型echarts圖,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04

