使用Vue实现防篡改的水印
作者:子辰Web草庐
我们在平时上网的时候会看到有些图片是加水印的:
像这种加水印的操作往往是后端来做的,不过有些站点要保护的知识产权类型比较多,不光是图片,可能还有视频或者文字。
对不同类型的东西去加这个水印,后端操作起来就可能比较麻烦,因为水印这个东西防君子不防小人,他要搞你的话始终能搞你。
所以我们水印的作用,就是给他做一个适当的限制,让他没有那么轻易的能搞到。
因此现在有些站点开始逐步的让前端来制作这个水印了。
如果你是用的是 React 来开发的话就比较简单了:
这个 Ant Design 这个库,它本身就有一个组件叫做 Watermark 水印组件,通过这个组件就可以给一个区域加上一个水印,非常的 so easy 开发成本极低,无论这个区域是图片还是文字或者视频,都无所谓。
但是如果你使用的是 Vue 来开发的话很遗憾,无论是 Element UI 还是 Ant Design Vue,都没有这个 Watermark 组件。
那么就需要我们自己手动的去编写,其实编写这个组件也并不复杂,主要是要考虑两个问题:
- 如何来生成水印
- 如何来防止篡改
如何生成水印
我们先来看第一步如何生成水印。
基本思路与准备
我们可以有这么一个思路:
比如我们要在上图的区域做水印,那么就在区域里加上一个 div,div 填充满整个区域,然后给这个 div 一张水印的背景图,然后让背景图重复就可以了。
这个背景图我们可以使用 canvas 来画。
所以基于这么一个思路,我们就可以写出这么一个代码结构:
我们引入封装的 Watermark 组件,里边传入任何内容,可以是文字也可以是视频,然后就给这个区域加上水印。
通过 text 传入水印的文本。
那么我们看看组件里咋写的:
<template> <div class="watermark-container"> <slot></slot> <!-- 我们要做的就是在这里添加一个 div,填充满整个区域,设置水印背景并且重复 --> </div> </template> <script setup> import useWatermarkBg from './useWatermarkBg'; // 定义一些基本的属性( 如果说你想开发的更加完善,可以加入更多的属性来适应你的要求 ) const props = defineProps({ text: { // 传入水印的文本 type: String, required: true, default: 'watermark', }, fontSize: { // 字体的大小 type: Number, default: 40, }, gap: { // 水印重复的间隔 type: Number, default: 20, }, }); // useWatermarkBg 函数用来创建一个 canvas 图片 // 将属性传递进去就返回个创建好的对象 const bg = useWatermarkBg(props); console.log('bg.value >>> ', bg.value) </script>
目前组件的代码还是比较简单,我们看一下 useWatermarkBg 返回的数据是什么:
这里打印了两个对象,是因为我们有两个水印区域,这个对象里有三个属性:
base64:表示 canvas 生成图片的 dataurl,到时候就可以用它来做背景 size:表示 canvas 的宽高 styleSize:表示 canvas 的 DPR,如果想要用非常清晰的尺寸的话就用这个,这个值和 window 的devicePixelRatio 有关,如果你不知道的话可以关注子辰,后期会更新相关的文章 。
那么我们看看 useWatermarkBg 函数是怎么写的,代码也很简单:
import { computed } from 'vue'; export default function useWatermarkBg (props) { return computed(() => { // 创建一个 canvas const canvas = document.createElement('canvas'); const devicePixelRatio = window.devicePixelRatio || 1; // 设置字体大小 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); // 旋转 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, }; }); }
现在基本的数据有了,我们就要生成一个水印的背景的 div,填充在合适的位置。
生成水印填充背景
<template> <div class="watermark-container"> <slot></slot> <!-- 我们要做的就是在这里添加一个 div,填充满整个区域,设置水印背景并且重复 --> </div> </template> <script setup> import useWatermarkBg from './useWatermarkBg'; const props = defineProps({ // ... }); const bg = useWatermarkBg(props); // 创建一个 div const div = document.createElement('div'); </script>
我们这里使用 document.createElement
生成一个 div,有同学可能会问,为什么不直接在填充的位置写一个 div 呢?因为不行,至于为什么不行看到后边就知道了,在最后进行解释,现在就使用 dom 来创建这个 div。
现在呢我们给这个 div 设置一些样式:
<script setup> import useWatermarkBg from './useWatermarkBg'; const props = defineProps({ // ... }); const bg = useWatermarkBg(props); const div = document.createElement('div'); // 获取到解构的值 const { base64, styleSize } = bg; // 背景设置为 base64 的图片 div.style.backgroundImage = `url(${base64})`; // 背景的大小设置为 styleSize div.style.backgroundSize = `${styleSize}px ${styleSize}px`; // 重复方式设置为 repeat div.style.backgroundRepeat = 'repeat'; // 设置子元素与父元素四个方向的间隔(这里设置为 0 的效果同宽高设置 100%) div.style.inset = 0; // z-index 设置为 9999 覆盖上去 div.style.zIndex = 9999; </script>
样式我们也只能通过上面的方式来添加,而不能直接写成 class,具体原因后边会解释。
接下来我们要把这个 div 添加到父元素里边去:
<template> <!-- 在父元素上添加 ref --> <div class="watermark-container" ref="parentRef"> <slot></slot> <!-- 添加一个div,填充满整个区域,设置水印背景,重复 --> </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>
你可以会发现我们这里使用的是 watchEffect 来判断是否能获取到父元素,而不是在 onMounted 里边,这是因为这一块会涉及到后边的防篡改,我们一会就知道了,现在暂且不用管就放在这里。
可以看到 div 已经被添加进去了,背景图以及属性都是有的,只不过这个 div 不是绝对定位,要填充满的话就得设置绝对定位:
<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; // 设置绝对定位 div.style.position = 'absolute'; // 设置点击穿漏,防止底部元素失去鼠标事件的交互 div.style.pointerEvents = 'none'; parentRef.value.appendChild(div); }); </script>
你看,现在这个水印就加上了,没有什么问题,那么第一步加水印就完成了。
接下来我们就要说第二步了,如何防篡改。
如何防篡改
用户会怎么来篡改我们的水印呢?他有很多办法,直接在页面上操作不太可能,他主要的办法就是进入这个浏览器调试工具,找到我们这个水印的 div 然后删除:
这样一删除就没了,所以我们仅仅是把这个水印生成出来毫无意义,因为可以轻松的删除。
那这就要求我们必须要找到某一种方式,能够监控用户对我们水印元素的操作,比如说删除。
所以这个防篡改就涉及到两件事:
- 如何监控
- 重新生成
这就解释清楚了为什么不直接在父元素里写 div 的原因,因为直接在父元素里写的话如果删除掉的话无法重新生成,但是通过 document 添加的话就可以。
把 div 放在 watchEffect 里边只要监控到用户动了水印,只要在执行一遍 watchEffect 就能重新生成一个新的水印添加进去。
如果说我们不是在 watchEffect 里还是在 onMounted 里就没办法那做到重新运行了。
同时也解释了为什么样式不能写在 class 里,因为在 calss 里的话,用户通过调试工具更改的话,我们同样无法监控到。
好了,刚才的三个疑问现在都解决了。
如何监控
现在的问题就是我们如何去监控的问题了,我们怎么知道用户动了水印呢?
那么这里就要说到一个 API 了,叫做 MutationObserve 它可以监控一个元素的变化,不仅可以监控元素本事,还可以监控元素里边所有的子元素,无论是改动元素的属性,还是元素的内容,这个 API 都可以收到通知。
我们现在就利用这会 API 来实现监控,首先我们要搞清楚的是,到底要监控谁,我们要监控的不是水印的 div,而是整个组件,这样就可以监控到所有的东西了。
所以我们可以这样写:
<script setup> import { ref, watchEffect, onMounted, onUnmounted } from 'vue'; // etc... let ob; onMounted(() => { // 在 onMounted 里边创建一个 MutationObserver 来进行监控 // 一旦某个东西有变化就会运行这个回调函数 ob = new MutationObserver((records) => { // 并把变化记录下来传递给我们 console.log('records >>> ', records) }); // 创建好监听器之后,告诉监听器需要监听的元素 ob.observe(parentRef.value, { // 监听的时候需要加一些配置 childList: true, // 元素内容有没有发生变化 attributes: true, // 元素本身的属性有没有发生变化 subtree: true, // 告诉它监控的是整个子树,就是包含整个子元素 }); }); // 在组件卸载的时候取消监听 onUnmounted(() => { ob && ob.disconnect(); // 取消监听 }); </script>
现在我们就基本设置好了,看一下效果如何:
在最开始的时候就打印了两次,因为我们添加了两次水印的 div,加这个 div 的动作就被监听到了。
返回值是一个数组,表示我们的操作动作,动作里边也明确的表示是添加节点,并且是 div 节点。
如果我们删除水印的 div,同样也触发了我们的回调函数,动作也记录到了我们删除了一个 div 的节点。
通过对动作的了解我们就可以知道如何来监控节点的删除,获取到删除的节点并且与我们添加的节点对比,就知道用户是否删除了我们的水印节点,我们就可以这样来写:
<script setup> // 将 div 保存在外部因为要判断节点时使用 let div; 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.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) => { // 循环节点的动作 for (const record of records) { // 如果有节点被删除,循环一下判断是否有水印的节点 for (const dom of record.removedNodes) { if (dom === div) { console.log('水印被删除') // ... return; } } // 如果有节点被修改,判断一下是否是水印的节点 if (record.target === div) { console.log('属性被修改') // ... return; } } }); ob.observe(parentRef.value, { childList: true, attributes: true, subtree: true, }); }); // 在组件卸载的时候取消监听 onUnmounted(() => { ob && ob.disconnect(); // 取消监听 div = null; // 因为 div 是全局变量在写在的时候值为空 }); </script>
水印删除后事件就被触发了。
属性被修改时同样会触发事件。
重新生成
那么我们能监控到事件了如何重新运行 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++; // 删除节点的时候更新依赖 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>
这样就可以完成了,只要监控到删除或者修改属性,就会重新运行 watchEffect 重新生成一个新的水印:
总结
水印是一种保护知识产权的手段,但是如果只是简单的生成水印,很容易被用户篡改或删除。
所以我们需要使用一些技巧来防止水印被破坏,比如使用 canvas 生成背景图,使用 document.createElement 添加水印元素,使用 MutationObserver 监控元素变化,使用 watchEffect 重新生成水印等。
这样我们就可以实现一个比较安全的水印组件,提高我们的网站的安全性和可信度。
像 Ant Design 里边的水印就是这样做的,沿着这个思路我们就可以一步一步的把这个组件给它完善掉。
以上就是使用Vue实现防篡改的水印的详细内容,更多关于Vue实现防篡改水印的资料请关注脚本之家其它相关文章!