使用Vue實(shí)現(xiàn)防篡改的水印
我們?cè)谄綍r(shí)上網(wǎng)的時(shí)候會(huì)看到有些圖片是加水印的:
像這種加水印的操作往往是后端來(lái)做的,不過(guò)有些站點(diǎn)要保護(hù)的知識(shí)產(chǎn)權(quán)類型比較多,不光是圖片,可能還有視頻或者文字。
對(duì)不同類型的東西去加這個(gè)水印,后端操作起來(lái)就可能比較麻煩,因?yàn)樗∵@個(gè)東西防君子不防小人,他要搞你的話始終能搞你。
所以我們水印的作用,就是給他做一個(gè)適當(dāng)?shù)南拗?,讓他沒(méi)有那么輕易的能搞到。
因此現(xiàn)在有些站點(diǎn)開(kāi)始逐步的讓前端來(lái)制作這個(gè)水印了。
如果你是用的是 React 來(lái)開(kāi)發(fā)的話就比較簡(jiǎn)單了:
這個(gè) Ant Design 這個(gè)庫(kù),它本身就有一個(gè)組件叫做 Watermark 水印組件,通過(guò)這個(gè)組件就可以給一個(gè)區(qū)域加上一個(gè)水印,非常的 so easy 開(kāi)發(fā)成本極低,無(wú)論這個(gè)區(qū)域是圖片還是文字或者視頻,都無(wú)所謂。
但是如果你使用的是 Vue 來(lái)開(kāi)發(fā)的話很遺憾,無(wú)論是 Element UI 還是 Ant Design Vue,都沒(méi)有這個(gè) Watermark 組件。
那么就需要我們自己手動(dòng)的去編寫,其實(shí)編寫這個(gè)組件也并不復(fù)雜,主要是要考慮兩個(gè)問(wèn)題:
- 如何來(lái)生成水印
- 如何來(lái)防止篡改
如何生成水印
我們先來(lái)看第一步如何生成水印。
基本思路與準(zhǔn)備
我們可以有這么一個(gè)思路:
比如我們要在上圖的區(qū)域做水印,那么就在區(qū)域里加上一個(gè) div,div 填充滿整個(gè)區(qū)域,然后給這個(gè) div 一張水印的背景圖,然后讓背景圖重復(fù)就可以了。
這個(gè)背景圖我們可以使用 canvas 來(lái)畫。
所以基于這么一個(gè)思路,我們就可以寫出這么一個(gè)代碼結(jié)構(gòu):
我們引入封裝的 Watermark 組件,里邊傳入任何內(nèi)容,可以是文字也可以是視頻,然后就給這個(gè)區(qū)域加上水印。
通過(guò) text 傳入水印的文本。
那么我們看看組件里咋寫的:
<template> <div class="watermark-container"> <slot></slot> <!-- 我們要做的就是在這里添加一個(gè) div,填充滿整個(gè)區(qū)域,設(shè)置水印背景并且重復(fù) --> </div> </template> <script setup> import useWatermarkBg from './useWatermarkBg'; // 定義一些基本的屬性( 如果說(shuō)你想開(kāi)發(fā)的更加完善,可以加入更多的屬性來(lái)適應(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ù)用來(lái)創(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í)候就可以用它來(lái)做背景 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'; // 將文字畫出來(lái) 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ì)問(wèn),為什么不直接在填充的位置寫一個(gè) div 呢?因?yàn)椴恍校劣跒槭裁床恍锌吹胶筮吘椭懒?,在最后進(jìn)行解釋,現(xiàn)在就使用 dom 來(lái)創(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>
樣式我們也只能通過(guò)上面的方式來(lái)添加,而不能直接寫成 class,具體原因后邊會(huì)解釋。
接下來(lái)我們要把這個(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(() => { // 獲取不到,就說(shuō)明還沒(méi)有掛載,先出去 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 來(lái)判斷是否能獲取到父元素,而不是在 onMounted 里邊,這是因?yàn)檫@一塊會(huì)涉及到后邊的防篡改,我們一會(huì)就知道了,現(xiàn)在暫且不用管就放在這里。
可以看到 div 已經(jīng)被添加進(jìn)去了,背景圖以及屬性都是有的,只不過(guò)這個(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è)水印就加上了,沒(méi)有什么問(wèn)題,那么第一步加水印就完成了。
接下來(lái)我們就要說(shuō)第二步了,如何防篡改。
如何防篡改
用戶會(huì)怎么來(lái)篡改我們的水印呢?他有很多辦法,直接在頁(yè)面上操作不太可能,他主要的辦法就是進(jìn)入這個(gè)瀏覽器調(diào)試工具,找到我們這個(gè)水印的 div 然后刪除:
這樣一刪除就沒(méi)了,所以我們僅僅是把這個(gè)水印生成出來(lái)毫無(wú)意義,因?yàn)榭梢暂p松的刪除。
那這就要求我們必須要找到某一種方式,能夠監(jiān)控用戶對(duì)我們水印元素的操作,比如說(shuō)刪除。
所以這個(gè)防篡改就涉及到兩件事:
- 如何監(jiān)控
- 重新生成
這就解釋清楚了為什么不直接在父元素里寫 div 的原因,因?yàn)橹苯釉诟冈乩飳懙脑捜绻麆h除掉的話無(wú)法重新生成,但是通過(guò) document 添加的話就可以。
把 div 放在 watchEffect 里邊只要監(jiān)控到用戶動(dòng)了水印,只要在執(zhí)行一遍 watchEffect 就能重新生成一個(gè)新的水印添加進(jìn)去。
如果說(shuō)我們不是在 watchEffect 里還是在 onMounted 里就沒(méi)辦法那做到重新運(yùn)行了。
同時(shí)也解釋了為什么樣式不能寫在 class 里,因?yàn)樵?calss 里的話,用戶通過(guò)調(diào)試工具更改的話,我們同樣無(wú)法監(jiān)控到。
好了,剛才的三個(gè)疑問(wèn)現(xiàn)在都解決了。
如何監(jiān)控
現(xiàn)在的問(wèn)題就是我們?nèi)绾稳ケO(jiān)控的問(wèn)題了,我們?cè)趺粗烙脩魟?dòng)了水印呢?
那么這里就要說(shuō)到一個(gè) API 了,叫做 MutationObserve 它可以監(jiān)控一個(gè)元素的變化,不僅可以監(jiān)控元素本事,還可以監(jiān)控元素里邊所有的子元素,無(wú)論是改動(dòng)元素的屬性,還是元素的內(nèi)容,這個(gè) API 都可以收到通知。
我們現(xiàn)在就利用這會(huì) API 來(lái)實(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 來(lái)進(jìn)行監(jiān)控 // 一旦某個(gè)東西有變化就會(huì)運(yùn)行這個(gè)回調(diào)函數(shù) ob = new MutationObserver((records) => { // 并把變化記錄下來(lái)傳遞給我們 console.log('records >>> ', records) }); // 創(chuàng)建好監(jiān)聽(tīng)器之后,告訴監(jiān)聽(tīng)器需要監(jiān)聽(tīng)的元素 ob.observe(parentRef.value, { // 監(jiān)聽(tīng)的時(shí)候需要加一些配置 childList: true, // 元素內(nèi)容有沒(méi)有發(fā)生變化 attributes: true, // 元素本身的屬性有沒(méi)有發(fā)生變化 subtree: true, // 告訴它監(jiān)控的是整個(gè)子樹(shù),就是包含整個(gè)子元素 }); }); // 在組件卸載的時(shí)候取消監(jiān)聽(tīng) onUnmounted(() => { ob && ob.disconnect(); // 取消監(jiān)聽(tīng) }); </script>
現(xiàn)在我們就基本設(shè)置好了,看一下效果如何:
在最開(kāi)始的時(shí)候就打印了兩次,因?yàn)槲覀兲砑恿藘纱嗡〉?div,加這個(gè) div 的動(dòng)作就被監(jiān)聽(tīng)到了。
返回值是一個(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)。
通過(guò)對(duì)動(dòng)作的了解我們就可以知道如何來(lái)監(jiān)控節(jié)點(diǎn)的刪除,獲取到刪除的節(jié)點(diǎn)并且與我們添加的節(jié)點(diǎn)對(duì)比,就知道用戶是否刪除了我們的水印節(jié)點(diǎn),我們就可以這樣來(lái)寫:
<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)聽(tīng) onUnmounted(() => { ob && ob.disconnect(); // 取消監(jiān)聽(tīng) 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)單的生成水印,很容易被用戶篡改或刪除。
所以我們需要使用一些技巧來(lái)防止水印被破壞,比如使用 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-10vue修改對(duì)象的屬性值后頁(yè)面不重新渲染的實(shí)例
今天小編就為大家分享一篇vue修改對(duì)象的屬性值后頁(yè)面不重新渲染的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08VUE+elementui組件在table-cell單元格中繪制微型echarts圖
這篇文章主要介紹了VUE+elementui組件在table-cell單元格中繪制微型echarts圖,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04