使用Vue实现防篡改的水印

 更新时间:2023年08月27日 08:53:02   作者:子辰Web草庐  
我们在平时上网的时候会看到有些图片是加水印的,一般水印往往是后端来做的,不过有些站点要保护的知识产权类型比较多,不光是图片,可能还有视频或者文字,所以我们水印的作用,就是给他做一个适当的限制,本文就给大家介绍一下如何使用Vue实现防篡改的水印

我们在平时上网的时候会看到有些图片是加水印的:

像这种加水印的操作往往是后端来做的,不过有些站点要保护的知识产权类型比较多,不光是图片,可能还有视频或者文字。

对不同类型的东西去加这个水印,后端操作起来就可能比较麻烦,因为水印这个东西防君子不防小人,他要搞你的话始终能搞你。

所以我们水印的作用,就是给他做一个适当的限制,让他没有那么轻易的能搞到。

因此现在有些站点开始逐步的让前端来制作这个水印了。

如果你是用的是 React 来开发的话就比较简单了:

这个 Ant Design 这个库,它本身就有一个组件叫做 Watermark 水印组件,通过这个组件就可以给一个区域加上一个水印,非常的 so easy 开发成本极低,无论这个区域是图片还是文字或者视频,都无所谓。

但是如果你使用的是 Vue 来开发的话很遗憾,无论是 Element UI 还是 Ant Design Vue,都没有这个 Watermark 组件。

那么就需要我们自己手动的去编写,其实编写这个组件也并不复杂,主要是要考虑两个问题:

  1. 如何来生成水印
  2. 如何来防止篡改

如何生成水印

我们先来看第一步如何生成水印。

基本思路与准备

我们可以有这么一个思路:

比如我们要在上图的区域做水印,那么就在区域里加上一个 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 然后删除:

这样一删除就没了,所以我们仅仅是把这个水印生成出来毫无意义,因为可以轻松的删除。

那这就要求我们必须要找到某一种方式,能够监控用户对我们水印元素的操作,比如说删除。

所以这个防篡改就涉及到两件事:

  1. 如何监控
  2. 重新生成

这就解释清楚了为什么不直接在父元素里写 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实现防篡改水印的资料请关注脚本之家其它相关文章!

相关文章

  • vue学习之Vue-Router用法实例分析

    vue学习之Vue-Router用法实例分析

    这篇文章主要介绍了vue学习之Vue-Router用法,结合实例形式分析了Vue-Router路由原理与常见操作技巧,需要的朋友可以参考下
    2020-01-01
  • 使用el-form之表单校验自动定位到报错位置问题

    使用el-form之表单校验自动定位到报错位置问题

    这篇文章主要介绍了使用el-form之表单校验自动定位到报错位置问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05
  • vue 子组件和父组件传值的示例

    vue 子组件和父组件传值的示例

    这篇文章主要介绍了vue 子组件和父组件传值的示例,帮助大家更好的理解和学习vue,感兴趣的朋友可以了解下
    2020-09-09
  • vue使用axios获取不到响应头Content-Disposition的问题及解决

    vue使用axios获取不到响应头Content-Disposition的问题及解决

    这篇文章主要介绍了vue使用axios获取不到响应头Content-Disposition的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • Vue框架中正确引入JS库的方法介绍

    Vue框架中正确引入JS库的方法介绍

    最近在学习使用vue框架,在使用中遇到了一个问题,查找相关资料终于找了正确的姿势,所以这篇文章主要给大家介绍了关于在Vue框架中正确引入JS库的方法,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-07-07
  • 关于element ui中的el-scrollbar横向滚动问题

    关于element ui中的el-scrollbar横向滚动问题

    这篇文章主要介绍了关于element ui中的el-scrollbar横向滚动问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • vue3使用svg图标的多种方式总结

    vue3使用svg图标的多种方式总结

    svg图片在项目中使用的非常广泛,下面这篇文章主要给大家介绍了关于vue3使用svg图标的多种方式,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-03-03
  • vue.js表格分页示例

    vue.js表格分页示例

    这篇文章主要为大家详细介绍了vue.js表格分页示例,ajax异步加载数据,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • Element-Plus之el-col与el-row快速布局

    Element-Plus之el-col与el-row快速布局

    el-col是el-row的子元素,下面这篇文章主要给大家介绍了关于Element-Plus之el-col与el-row快速布局的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-09-09
  • Vue实现剪贴板复制功能

    Vue实现剪贴板复制功能

    这篇文章主要介绍了Vue实现剪贴板复制功能,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-12-12

最新评论