页面水印的实现以及防删除方案

水印相关

  • 引言
  • 绘制一个水印
  • 输出背景图
  • 封装一点点细节
  • 图片加水印
  • 防止水印删除
    • 问题
    • 解决方案

引言

在企业里为了防止信息泄露和保护知识产权,通常会在页面和图片上添加水印
前端页面水印的添加一般有这几种方式:dom 元素循环、canvas 输出背景图、svg 实现背景图、图片添加水印

dom 元素循环 性能太低也不优雅,一般不采用这种方式
svg 实现背景图 与 canvas 类似,且兼容性不如 canvas
图片添加水印 是针对在图片上加的水印

本篇文章重点讲一下canvas 输出背景图、以及防止删除的方案来生成水印方式

绘制一个水印

目标是实现页面上按照一定的排列展示的水印,那么首先要用 canvas 画出一个水印

我们先在 html 放一个 canvas 标签,对它进行一个绘制

<template><canvas id="water"></canvas>
</template>
onMounted(() => {const canvas: HTMLCanvasElement = document.getElementById('water') as HTMLCanvasElementcanvas.width = 440canvas.height = 400const ctx = canvas.getContext('2d')if (ctx) {ctx.font = '60px PingFang SC'ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'ctx.rotate(-0.4)ctx.fillText('krryguo', 20, 280)}
})

页面效果如图:
请添加图片描述
一个水印画出来了,怎么衍生多个水印并添加到指定页面中呢?

输出背景图

直接循环多份代码绘制是不合理的。我们可以利用 background 属性 repeat 特性,将水印展示成多个且平铺在整个页面中

将 canvas 生成的画布输出成base64的字符串,来作为页面的背景图。那么 dom 结构可以不需要 canvas 元素了,动态生成即可

<template><div class="water-mark">首页</div>
</template>
onMounted(() => {const canvas: HTMLCanvasElement = document.createElement('canvas')canvas.width = 440canvas.height = 400const ctx = canvas.getContext('2d')if (ctx) {ctx.font = '60px PingFang SC'ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'ctx.rotate(-0.4)ctx.fillText('krryguo', 40, 200)}const imgStr = canvas.toDataURL('image/png')const waterDom = document.getElementsByClassName('water-mark')[0] as HTMLElementwaterDom.style.background = `url(${imgStr})`
})

实现的效果图:
请添加图片描述
看到这里,有朋友不高兴了,排列的太整齐,你这水印有问题啊~

最简单解决方式就直接绘制 两个斜对称排列 的水印即可

onMounted(() => {const canvas: HTMLCanvasElement = document.createElement('canvas')canvas.width = 880 // 原有的基础上增加一倍宽度canvas.height = 400const ctx = canvas.getContext('2d')if (ctx) {ctx.font = '60px PingFang SC'ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'ctx.rotate(-0.4)ctx.fillText('krryguo', 40, 200)ctx.fillText('krryguo', 350, 556) // 再画一个}const imgStr = canvas.toDataURL('image/png')const waterDom = document.getElementsByClassName('water-mark')[0] as HTMLElementwaterDom.style.background = `url(${imgStr})`
})

再看效果图:
请添加图片描述

封装一点点细节

interface WatermarkOptions {// 宽度width?: number// 高度height?: number// 水印内容content?: string// 水印字体font?: string// 水印颜色color?: string// 透明度opacity?: number// 偏转角度degree?: number// 偏移量x1?: numbery1?: numberx2?: numbery2?: number
}const createWatermark = ({width = 880,height = 400,content = 'krryguo',font = '60px PingFang SC',color = 'rgba(156, 162, 169, 0.3)',opacity = 1,degree = -23,x1 = 40,y1 = 200,x2 = 350,y2 = 556
}: WatermarkOptions): string => {const canvas: HTMLCanvasElement = document.createElement('canvas')canvas.width = widthcanvas.height = heightconst ctx = canvas.getContext('2d')if (ctx) {ctx.font = fontctx.fillStyle = colorctx.globalAlpha = opacity// 顺时针旋转的弧度,计算公式: degree * Math.PI / 180ctx.rotate((degree * Math.PI) / 180)ctx.fillText(content, x1, y1)ctx.fillText(content, x2, y2)}return canvas.toDataURL('image/png')
}const setWatermarkClass = (url: string, className: string): void => {const style = document.createElement('style')style.innerHTML = `.${className} {background-image: url(${url});}`document.head.appendChild(style)
}

可在有需要加水印的地方调用 setWatermarkClass,传入自定义配置和指定的 class 名,再在对应元素设置该 class 名即可加上水印

onMounted(() => {setWatermarkClass(createWatermark({content: 'krryblog'}),'my-water-mark')
})
<template><!-- 这里加上 my-water-mark 水印的类名 --><div class="water-mark my-water-mark">首页</div>
</template>

图片加水印

同理,先读取图片,canvas 在图片上绘制水印

import waterUrl from '@/assets/water.jpeg'const createImgWatermark = async ({url = '',textAlign = 'center',textBaseline = 'middle',font = '30px PingFang SC',fillStyle = '#fff',x = 120,y = 50,position = 'top-start'},content: string = '这是水印'
) => {const canvas: HTMLCanvasElement = document.createElement('canvas')const img = new Image()img.src = urlimg.setAttribute('crossOrigin', 'Anonymous')return new Promise((resolve) => {img.onload = () => {canvas.width = img.widthcanvas.height = img.heightconst ctx = canvas.getContext('2d')if (ctx) {ctx.drawImage(img, 0, 0)ctx.textAlign = textAlignctx.textBaseline = textBaselinectx.font = fontctx.fillStyle = fillStyleswitch (position) {case 'top-end':x = img.width - xbreakcase 'bottom-start':y = img.height - ybreakcase 'bottom-end':x = img.width - xy = img.height - ybreak}ctx.fillText(content, x, y)}resolve(canvas.toDataURL())}})
}const setImgWatermark = (url: string, dom: HTMLImageElement) => {dom.src = url
}onMounted(async () => {const url = await createImgWatermark({url: waterUrl,font: '50px PingFang SC',x: 160,y: 70,position: 'bottom-end'})setImgWatermark(url, document.querySelector('img') as HTMLImageElement)
})
<template><img width="600" />
</template>

效果图:
请添加图片描述

防止水印删除

前端生成水印的安全性是很弱的,懂点前端知识的人都会打开控制台修改去掉水印

这里提供一个方案,禁止用户删除 class 来防止水印删除

window 提供了一个监听器MutationObserver:监视对 DOM 树所做更改的能力
API 方法

  • disconnect()
    阻止 MutationObserver 实例继续接收的通知,直到再次调用其 observe()方法,该观察者对象包含的回调函数都不会再被调用
  • observe()
    配置 MutationObserver 在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知
  • takeRecords()
    从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中

先获取有水印类名的NodeList,遍历所有元素为其添加一个MutationObserver监听器来监听 dom 属性的变化,获取目标元素的classList,若不存在水印的 class 类名,就执行添加。执行添加之后要立刻暂停监听disconnect(),防止【添加】操作又触发监听器,最后再执行observe() 重新观察

// 添加监听器
const addListioner = (className: string) => {const MutationObserver = window.MutationObserver// 获取所有添加了水印类名的 domconst containerList: NodeListOf<HTMLElement> = document.querySelectorAll(`.${className}`)if (MutationObserver) {containerList.forEach((container) => {let observer = new MutationObserver(() => {// 获取 class 集合const classList: DOMTokenList = container.classListif (!Object.values(classList).includes(className)) {// 如果 classList 中不存在水印的类名,就重新添加container.classList.add(className)// 暂停监听,防止上面的操作又触发监听器observer.disconnect()// 然后再重新开始观察addObserve(observer, container)}})// 每个元素开启观察addObserve(observer, container)})}
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {mutation.observe(container, {// 观察器的配置,需要观察属性的变动attributes: true})
}

然后在 onMounted 添加一下监听器

onMounted(async () => {// TODO ...addListioner('my-water-mark')
})

问题

但是这又有一个问题,用户可以在控制台改变水印 class 里面的样式,而 MutationObserver 无法监听。如图:
请添加图片描述
这样水印说没就直接没啦

解决方案

解决方法:可以使用 style 属性 渲染水印,即 内联样式 ,这样若样式改变了就说明 dom 属性改变,也就可以监听到了

需要注意的是:要设置 !important,把优先级提到最高防止被恶意覆盖,后面的监听也要加上优先级的判断

onMounted(async () => {const bgUrl = createWatermark({content: 'krryblog'})// 使用 style 属性渲染水印const dom = document.querySelector('.water-mark-style') as HTMLElement// 设置样式优先级最高dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')
})

缺点是控制台查看 dom 结构会有一大坨样式在这里…
请添加图片描述
最后再加上监听 顺带整合了 class 类名渲染、style 属性渲染两种监听方法

interface StyleType {key: stringvalue: string
}onMounted(async () => {const bgUrl = createWatermark({content: 'krryblog'})// 使用 style 属性渲染水印const dom = document.querySelector('.water-mark-style') as HTMLElement// 设置样式优先级最高dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')addListioner('water-mark-style', {key: 'background-image',value: `url("${bgUrl}")` // js 获取的样式值 url 里面加了 "",所以这里加上比对})
})// 添加监听器
const addListioner = (className: string, style?: StyleType) => {const MutationObserver = window.MutationObserver// 获取所有添加了水印类名的 domconst containerList: NodeListOf<HTMLElement> = document.querySelectorAll(`.${className}`)if (MutationObserver) {containerList.forEach((container: HTMLElement) => {// 每个元素监听const observer = new MutationObserver(() => {let flag = false // 触发改变的标识if (style) {// style 属性渲染水印// 获取 style 属性const styleCss: CSSStyleDeclaration = container.style// 需要比对样式是否存在、样式值是否相同、样式优先级是否最高if (!styleCss.getPropertyValue(style.key) ||styleCss.getPropertyValue(style.key) !== style.value ||styleCss.getPropertyPriority(style.key) !== 'important') {// 重新设置样式styleCss.setProperty(style.key, style.value, 'important')flag = true}} else {// class 类名渲染水印// 获取 class 集合const classList: DOMTokenList = container.classListif (!Object.values(classList).includes(className)) {// 如果 classList 中不存在水印的类名,就重新添加container.classList.add(className)flag = true}}if (flag) {// 暂停监听,防止上面的操作又触发监听器observer.disconnect()// 然后再重新开始观察addObserve(observer, container)}})// 每个元素开启观察addObserve(observer, container)})}
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {mutation.observe(container, {// 观察器的配置,需要观察属性的变动attributes: true})
}

监听效果查看:
请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/53611.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

企业财税自动化解决方案的成本效益分析与投资回报预测

随着企业规模的扩大和业务复杂度的增加&#xff0c;企业在财务管理方面也面临着诸多挑战&#xff0c;传统的财务管理方式逐渐无法满足企业经营需求&#xff0c;借助财税自动化解决方案来提高财务效率和准确性、降低人力成本&#xff0c;为企业带来长期的效益提升&#xff0c;已…

通过战略性SEO整合提升B2B内容的可见性、权威性和投资回报率

精明的市场营销者知道&#xff0c;尽管B2B营销的重点是推动商业采购&#xff0c;但归根结底&#xff0c;商务买家依然是消费者。无论你是针对返校购物的父母的鞋子品牌&#xff0c;还是寻找新的内容制作合作伙伴以扩大内容计划的市场营销领导者&#xff0c;搜索引擎优化&#x…

航空航天混合动力(2)电动飞机发展中的电气连接挑战

航空航天混合动力(2)电动飞机发展中的电气连接挑战 1.概述2.更高的电压(千伏)和功率(兆瓦)2.缓解局部放电/电晕效应3.提高可靠性,更恶劣的环境,频繁的维护和检查,冲击和振动4.减重5.抗电弧跟踪和液压油阻力6.严格的可燃性,毒性和烟雾要求7.海拔和气压的影响8.工作温度范围…

远程存储 RDMA

什么是 RDMA RDMA&#xff08;Remote Direct Memory Access&#xff09;指的是远程直接内存访问&#xff0c;这是一种通过网络在两个应用程序之间搬运缓冲区里的数据的方法。 Remote&#xff1a;数据通过网络与远程机器间进行数据传输。 Direct&#xff1a;没有内核的参与&am…

【区块链 + 人才服务】CERX- 基于联盟链的研学资源交换网络 | FISCO BCOS应用案例

CERX 是定位于面向高校科学研究与教学的分布式研学资产交换网络&#xff0c;构建一个用于数据、算法模型、论文和课程的研学资源价值流转平台。该平台采用 FISCO BCOS 联盟链为核心区块链层。 CERX 基于“交叉学科”的人才培养生态&#xff0c;围绕“科研、课件、课程、证书”…

Linux基础3-基础工具1(什么是工具,yum,vim基础)

目录 一.什么是工具 二.yum 2.1 yum基础 2.2 yum拓展 2.3 rzsz 三.vim基础 四.下章内容 1. vim 插入模式&#xff0c;底行模式&#xff0c;命令模式下详解。vim基础配置 2. gcc/g 基础 一.什么是工具 工具的本质是也是指令。通过工具我们能快速的实现某些功能 二.yum 2.1…

JVM合集

序言: 1.什么是JVM? JVM就是将javac编译后的.class字节码文件翻译为操作系统能执行的机器指令翻译过程: 前端编译:生成.class文件就是前端编译后端编译:通过jvm解释(或即时编译或AOT)执行.class文件时跨平台的,jvm并不是跨平台的通过javap进行反编译2.java文件是怎么变…

任意论文一键变播客,谷歌正式发布Illuminate,它能重构研究者的学习方式吗?

先来听一段英文播客&#xff0c;内容是不是很熟悉&#xff1f; &#xff0c;时长04:27 是的&#xff0c;这俩人就是在聊那篇《Attention is All You Need》。在 4 分半的对话里&#xff0c;他们介绍了论文的核心内容&#xff0c;一问一答&#xff0c;听上去相当自然。 播客原址…

SpringBoot大学生租房平台:技术实现与市场分析

第2章 开发环境与技术 大学生租房平台的编码实现需要搭建一定的环境和使用相应的技术&#xff0c;接下来的内容就是对大学生租房平台用到的技术和工具进行介绍。 2.1 MYSQL数据库 本课题所开发的应用程序在数据操作方面是不可预知的&#xff0c;是经常变动的&#xff0c;没有办…

[最优化方法] 《最优化方法》个人问答式学习笔记 with LLM

《最优化方法》问答式学习笔记 with LLM 文章目录 《最优化方法》问答式学习笔记 with LLM写在前面每周提问的链接表格绪论 | 第一周 | [answer by 文心一言]Q1 请为我解释一下最优化方法研究的核心重点主要是哪些&#xff1f;一、问题定义与建模二、求解方法三、算法性能与优化…

HTML5+CSS+JS制作中秋佳节页面

HTML5CSSJS制作中秋佳节页面 中秋节&#xff0c;是中国民间的传统节日。每年农历八月十五庆祝。 在中秋节这一天&#xff0c;人们会通过各种方式庆祝&#xff0c;其中最重要的活动之一就是赏月。家人团聚在一起&#xff0c;共同欣赏明亮的月亮。同时&#xff0c;吃月饼也是中秋…

Delphi 12.1安卓APP开发中获取硬件信息及手机号

Demo与代码已上传到CSDN下载。 这里简单说一下代码内容&#xff0c;完整代码请自行下载&#xff0c;不清楚的欢迎留言交流。 前言 演示Demo使用了我自己开发的一个控件&#xff0c;TLayoutPro 《Delphi D10.3 LayoutsPro 控件简介 -避免输入焦点被虚拟键盘遮挡》请查看并下载控…

2024年【上海市安全员C证】考试题库及上海市安全员C证报名考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【上海市安全员C证】考试题库及上海市安全员C证报名考试&#xff0c;包含上海市安全员C证考试题库答案和解析及上海市安全员C证报名考试练习。安全生产模拟考试一点通结合国家上海市安全员C证考试最新大纲及上海…

Vue 3 + Element Plus 封装单列控制编辑的可编辑表格组件

在Web应用开发中&#xff0c;经常需要提供表格数据的编辑功能。本文将介绍如何使用Vue 3结合Element Plus库来实现一个支持单列控制编辑功能的表格&#xff0c;并通过封装组件的形式提高代码的复用性。通过本教程&#xff0c;你将学会如何构建一个具备单列控制编辑功能的表格组…

Vue2中使用ant-design的tab组件让他一行充满

使用tabs组件默认样式这样 想改成水平居中铺满如下&#xff1a; 需要改下css样式 /deep/ .ant-tabs-nav {width: 100%;& > div {width: 100%;display: flex;align-items: center;}.ant-tabs-tab {flex: 1;text-align: center;}}

为Hexo添加说说功能—Artitalk

文章目录 Artitalk部署LeanCloud配置Hexo Artitalk 基于 LeanCloud 实现的可实时发布说说/微语的 js Artitalk.js官方文档 官方文档其实讲述的很详细了&#xff0c;在此记录一下&#xff0c;方便以后维护。 另外欢迎来我的博客 火柴人儿的小站&#xff0c;本博客基于雨云服务器…

15.2 JDBC数据库编程2

15.2.1 数据库访问步骤 使用JDBC API连接和访问数据库&#xff0c;一般分为以下5个步骤: (1) 加载驱动程序 (2) 建立连接对象 (3) 创建语句对象 (4) 获得SQL语句的执行结果 (5) 关闭建立的对象&#xff0c;释放资源 下面将详细描述这些步骤 15.2.2 加载驱动程序 要使…

山东省行政执法证照片要求及图像处理方法

在山东省&#xff0c;行政执法证是执法人员身份的重要标识&#xff0c;其照片的规范性对于证件的有效性至关重要。本文将详细介绍山东省行政执法证照片的要求&#xff0c;并提供使用手机相机拍照的实用方法&#xff0c;以确保照片符合标准。 一、山东省行政人员执法证照片拍摄要…

百度静态资源瓦片nginx直接显示完整案例

案例地址&#xff1a;https://download.csdn.net/download/jinhuding/89733763 访问显示效果&#xff1a;(根据瓦片地址直接显示) http://172.16.39.203:8099/tiles/

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站&#xff0c;手机流量可以访问IPV6网络的服务&#xff0c;为什么不在电脑搭建Home Assistant&am…