使用Vue實現(xiàn)防篡改的水印
我們在平時上網(wǎng)的時候會看到有些圖片是加水印的:
像這種加水印的操作往往是后端來做的,不過有些站點要保護的知識產(chǎn)權(quán)類型比較多,不光是圖片,可能還有視頻或者文字。
對不同類型的東西去加這個水印,后端操作起來就可能比較麻煩,因為水印這個東西防君子不防小人,他要搞你的話始終能搞你。
所以我們水印的作用,就是給他做一個適當?shù)南拗?,讓他沒有那么輕易的能搞到。
因此現(xiàn)在有些站點開始逐步的讓前端來制作這個水印了。
如果你是用的是 React 來開發(fā)的話就比較簡單了:
這個 Ant Design 這個庫,它本身就有一個組件叫做 Watermark 水印組件,通過這個組件就可以給一個區(qū)域加上一個水印,非常的 so easy 開發(fā)成本極低,無論這個區(qū)域是圖片還是文字或者視頻,都無所謂。
但是如果你使用的是 Vue 來開發(fā)的話很遺憾,無論是 Element UI 還是 Ant Design Vue,都沒有這個 Watermark 組件。
那么就需要我們自己手動的去編寫,其實編寫這個組件也并不復雜,主要是要考慮兩個問題:
- 如何來生成水印
- 如何來防止篡改
如何生成水印
我們先來看第一步如何生成水印。
基本思路與準備
我們可以有這么一個思路:
比如我們要在上圖的區(qū)域做水印,那么就在區(qū)域里加上一個 div,div 填充滿整個區(qū)域,然后給這個 div 一張水印的背景圖,然后讓背景圖重復就可以了。
這個背景圖我們可以使用 canvas 來畫。
所以基于這么一個思路,我們就可以寫出這么一個代碼結(jié)構(gòu):
我們引入封裝的 Watermark 組件,里邊傳入任何內(nèi)容,可以是文字也可以是視頻,然后就給這個區(qū)域加上水印。
通過 text 傳入水印的文本。
那么我們看看組件里咋寫的:
<template> <div class="watermark-container"> <slot></slot> <!-- 我們要做的就是在這里添加一個 div,填充滿整個區(qū)域,設(shè)置水印背景并且重復 --> </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: { // 水印重復的間隔 type: Number, default: 20, }, }); // useWatermarkBg 函數(shù)用來創(chuàng)建一個 canvas 圖片 // 將屬性傳遞進去就返回個創(chuàng)建好的對象 const bg = useWatermarkBg(props); console.log('bg.value >>> ', bg.value) </script>
目前組件的代碼還是比較簡單,我們看一下 useWatermarkBg 返回的數(shù)據(jù)是什么:
這里打印了兩個對象,是因為我們有兩個水印區(qū)域,這個對象里有三個屬性:
base64:表示 canvas 生成圖片的 dataurl,到時候就可以用它來做背景 size:表示 canvas 的寬高 styleSize:表示 canvas 的 DPR,如果想要用非常清晰的尺寸的話就用這個,這個值和 window 的devicePixelRatio 有關(guān),如果你不知道的話可以關(guān)注子辰,后期會更新相關(guān)的文章 。
那么我們看看 useWatermarkBg 函數(shù)是怎么寫的,代碼也很簡單:
import { computed } from 'vue'; export default function useWatermarkBg (props) { return computed(() => { // 創(chuàng)建一個 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ù)有了,我們就要生成一個水印的背景的 div,填充在合適的位置。
生成水印填充背景
<template> <div class="watermark-container"> <slot></slot> <!-- 我們要做的就是在這里添加一個 div,填充滿整個區(qū)域,設(shè)置水印背景并且重復 --> </div> </template> <script setup> import useWatermarkBg from './useWatermarkBg'; const props = defineProps({ // ... }); const bg = useWatermarkBg(props); // 創(chuàng)建一個 div const div = document.createElement('div'); </script>
我們這里使用 document.createElement
生成一個 div,有同學可能會問,為什么不直接在填充的位置寫一個 div 呢?因為不行,至于為什么不行看到后邊就知道了,在最后進行解釋,現(xiàn)在就使用 dom 來創(chuàng)建這個 div。
現(xiàn)在呢我們給這個 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`; // 重復方式設(shè)置為 repeat div.style.backgroundRepeat = 'repeat'; // 設(shè)置子元素與父元素四個方向的間隔(這里設(shè)置為 0 的效果同寬高設(shè)置 100%) div.style.inset = 0; // z-index 設(shè)置為 9999 覆蓋上去 div.style.zIndex = 9999; </script>
樣式我們也只能通過上面的方式來添加,而不能直接寫成 class,具體原因后邊會解釋。
接下來我們要把這個 div 添加到父元素里邊去:
<template> <!-- 在父元素上添加 ref --> <div class="watermark-container" ref="parentRef"> <slot></slot> <!-- 添加一個div,填充滿整個區(qū)域,設(shè)置水印背景,重復 --> </div> </template> <script setup> import { ref, watchEffect } from 'vue'; import useWatermarkBg from './useWatermarkBg'; const props = defineProps({ // .... }); // 聲明一個 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>
你可以會發(fā)現(xiàn)我們這里使用的是 watchEffect 來判斷是否能獲取到父元素,而不是在 onMounted 里邊,這是因為這一塊會涉及到后邊的防篡改,我們一會就知道了,現(xiàn)在暫且不用管就放在這里。
可以看到 div 已經(jīng)被添加進去了,背景圖以及屬性都是有的,只不過這個 div 不是絕對定位,要填充滿的話就得設(shè)置絕對定位:
<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è)置絕對定位 div.style.position = 'absolute'; // 設(shè)置點擊穿漏,防止底部元素失去鼠標事件的交互 div.style.pointerEvents = 'none'; parentRef.value.appendChild(div); }); </script>
你看,現(xiàn)在這個水印就加上了,沒有什么問題,那么第一步加水印就完成了。
接下來我們就要說第二步了,如何防篡改。
如何防篡改
用戶會怎么來篡改我們的水印呢?他有很多辦法,直接在頁面上操作不太可能,他主要的辦法就是進入這個瀏覽器調(diào)試工具,找到我們這個水印的 div 然后刪除:
這樣一刪除就沒了,所以我們僅僅是把這個水印生成出來毫無意義,因為可以輕松的刪除。
那這就要求我們必須要找到某一種方式,能夠監(jiān)控用戶對我們水印元素的操作,比如說刪除。
所以這個防篡改就涉及到兩件事:
- 如何監(jiān)控
- 重新生成
這就解釋清楚了為什么不直接在父元素里寫 div 的原因,因為直接在父元素里寫的話如果刪除掉的話無法重新生成,但是通過 document 添加的話就可以。
把 div 放在 watchEffect 里邊只要監(jiān)控到用戶動了水印,只要在執(zhí)行一遍 watchEffect 就能重新生成一個新的水印添加進去。
如果說我們不是在 watchEffect 里還是在 onMounted 里就沒辦法那做到重新運行了。
同時也解釋了為什么樣式不能寫在 class 里,因為在 calss 里的話,用戶通過調(diào)試工具更改的話,我們同樣無法監(jiān)控到。
好了,剛才的三個疑問現(xiàn)在都解決了。
如何監(jiān)控
現(xiàn)在的問題就是我們?nèi)绾稳ケO(jiān)控的問題了,我們怎么知道用戶動了水印呢?
那么這里就要說到一個 API 了,叫做 MutationObserve 它可以監(jiān)控一個元素的變化,不僅可以監(jiān)控元素本事,還可以監(jiān)控元素里邊所有的子元素,無論是改動元素的屬性,還是元素的內(nèi)容,這個 API 都可以收到通知。
我們現(xiàn)在就利用這會 API 來實現(xiàn)監(jiān)控,首先我們要搞清楚的是,到底要監(jiān)控誰,我們要監(jiān)控的不是水印的 div,而是整個組件,這樣就可以監(jiān)控到所有的東西了。
所以我們可以這樣寫:
<script setup> import { ref, watchEffect, onMounted, onUnmounted } from 'vue'; // etc... let ob; onMounted(() => { // 在 onMounted 里邊創(chuàng)建一個 MutationObserver 來進行監(jiān)控 // 一旦某個東西有變化就會運行這個回調(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)聽的時候需要加一些配置 childList: true, // 元素內(nèi)容有沒有發(fā)生變化 attributes: true, // 元素本身的屬性有沒有發(fā)生變化 subtree: true, // 告訴它監(jiān)控的是整個子樹,就是包含整個子元素 }); }); // 在組件卸載的時候取消監(jiān)聽 onUnmounted(() => { ob && ob.disconnect(); // 取消監(jiān)聽 }); </script>
現(xiàn)在我們就基本設(shè)置好了,看一下效果如何:
在最開始的時候就打印了兩次,因為我們添加了兩次水印的 div,加這個 div 的動作就被監(jiān)聽到了。
返回值是一個數(shù)組,表示我們的操作動作,動作里邊也明確的表示是添加節(jié)點,并且是 div 節(jié)點。
如果我們刪除水印的 div,同樣也觸發(fā)了我們的回調(diào)函數(shù),動作也記錄到了我們刪除了一個 div 的節(jié)點。
通過對動作的了解我們就可以知道如何來監(jiān)控節(jié)點的刪除,獲取到刪除的節(jié)點并且與我們添加的節(jié)點對比,就知道用戶是否刪除了我們的水印節(jié)點,我們就可以這樣來寫:
<script setup> // 將 div 保存在外部因為要判斷節(jié)點時使用 let div; watchEffect(() => { if (!parentRef.value) { return; } // 判斷之前的節(jié)點是否有內(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é)點的動作 for (const record of records) { // 如果有節(jié)點被刪除,循環(huán)一下判斷是否有水印的節(jié)點 for (const dom of record.removedNodes) { if (dom === div) { console.log('水印被刪除') // ... return; } } // 如果有節(jié)點被修改,判斷一下是否是水印的節(jié)點 if (record.target === div) { console.log('屬性被修改') // ... return; } } }); ob.observe(parentRef.value, { childList: true, attributes: true, subtree: true, }); }); // 在組件卸載的時候取消監(jiān)聽 onUnmounted(() => { ob && ob.disconnect(); // 取消監(jiān)聽 div = null; // 因為 div 是全局變量在寫在的時候值為空 }); </script>
水印刪除后事件就被觸發(fā)了。
屬性被修改時同樣會觸發(fā)事件。
重新生成
那么我們能監(jiān)控到事件了如何重新運行 watchEffect 呢?因為 watchEffect 是收集依賴的,只要依賴變化了它就會重新運行,所以我們可以手動搞一個依賴:
<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); // 聲明一個依賴 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é)點的時候更新依賴 return; } } if (record.target === div) { flag.value++; // 修改屬性的時候更新依賴 return; } } }); ob.observe(parentRef.value, { childList: true, attributes: true, subtree: true, }); }); onUnmounted(() => { ob && ob.disconnect(); div = null; }); </script>
這樣就可以完成了,只要監(jiān)控到刪除或者修改屬性,就會重新運行 watchEffect 重新生成一個新的水?。?/p>
總結(jié)
水印是一種保護知識產(chǎn)權(quán)的手段,但是如果只是簡單的生成水印,很容易被用戶篡改或刪除。
所以我們需要使用一些技巧來防止水印被破壞,比如使用 canvas 生成背景圖,使用 document.createElement 添加水印元素,使用 MutationObserver 監(jiān)控元素變化,使用 watchEffect 重新生成水印等。
這樣我們就可以實現(xiàn)一個比較安全的水印組件,提高我們的網(wǎng)站的安全性和可信度。
像 Ant Design 里邊的水印就是這樣做的,沿著這個思路我們就可以一步一步的把這個組件給它完善掉。
以上就是使用Vue實現(xiàn)防篡改的水印的詳細內(nèi)容,更多關(guān)于Vue實現(xiàn)防篡改水印的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
VUE+elementui組件在table-cell單元格中繪制微型echarts圖
這篇文章主要介紹了VUE+elementui組件在table-cell單元格中繪制微型echarts圖,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-04-04