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

水印相关

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

引言

在企业里为了防止信息泄露和保护知识产权,通常会在页面和图片上添加水印
前端页面水印的添加一般有这几种方式: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;已…

vue选择本地文件

需求&#xff1a;通过页面的按钮实现本地文件的选择&#xff08;以选择歌曲为例&#xff09;。 思路&#xff1a;利用原生input的type"file"来实现。 1.先自定义上传按钮 //HTML部分 <div class"p10 bs mb10" style"height: 40px"><…

通过战略性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…

Android app偶发Fragment中的Button点击事件无效

1.前言 在进行app开发的过程中,在某个Tv平台开发app的过程中,会使用到Tab切换主页面的功能, 所以就需要用到Fragment来切换页面,同时在Fragment中添加点击事件的时候,有时候无 响应,接下来就分析下具体原因 2.app偶发Fragment中的Button点击事件无效的核心功能分析 Fr…

【区块链 + 人才服务】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;听上去相当自然。 播客原址…

OAuth2.0和JWT

OAuth2.0 阮一峰的 理解OAuth 2.0 通俗的讲解了OAuth 2.0 的设计思路和运行流程 简单来说&#xff0c;OAuth&#xff08;开放授权&#xff09;是一种开放标准&#xff0c;用于允许用户在不暴露其凭据&#xff08;如用户名和密码&#xff09;的情况下&#xff0c;让第三方应用…

OPENAIGC开发者大赛企业组金奖 | 深度融合大语言模型的企业级智能文本审核系统

在第二届拯救者杯OPENAIGC开发者大赛中&#xff0c;涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到&#xff0c;我们特意开设了优秀作品报道专栏&#xff0c;旨在展示其独特之处和开发者的精彩故事。 无论您是技术专家还是爱好者&#xff0c;希望能带给您…

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

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

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

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

【Sqlite】.NET Framework使用Sqlite的注意事项

注意&#xff1a;NuGet引入System.Data.SQLite.Core不要引入System.Data.SQLite 注意&#xff1a;局域网共享链接 正常链接Data Source\\BAT-OCV\SqliteDB\batOCV.db;Version3;PoolingTrue;Max Pool Size100; 局域网链接Data Source\\\BAT-OCV\SqliteDB\batOCV.db;Version3;P…

qt怎么格式化字符串?

在Qt中&#xff0c;格式化字符串可以通过多种方式实现&#xff0c;主要依赖于你的具体需求和上下文。下面列出了一些常见的方法&#xff1a; 1. 使用QString的arg()方法 QString类提供了arg()方法&#xff0c;这个方法允许你插入值到字符串中的占位符位置。占位符由%1、%2等表…

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;你将学会如何构建一个具备单列控制编辑功能的表格组…