前端水印使用指南

一、背景

用户在使用系统的时候,有些数据是有权限的用户才能查看,有权限查看数据的用户在查看数据的时候,把数据截图发给了没有权限查看的用户,然后数据泄露了,当截图多次流转后就逐渐不知道最初是谁截的图,也就无从提醒截图的人要注意数据保密了。

前端水印技术,作为一种在网页上动态生成水印的解决方案,能够有效地在数据展示页面上添加个性化标记。此外,水印的存在本身也是一种警示,提醒每一位用户尊重数据隐私,遵守公司规定,共同维护企业信息安全。

二、水印实现方案

方案1:重复dom元素覆盖

在页面的body上插入一个position:fixed的div盒子,设置pointer-events: none; css实现盒子的点击穿透,在这个div内通过js循环生成小水印div,每个水印div内显示的水印内容,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>重复dom元素水印</title>
</head>
<style>
</style>
<body>
    <div class="app">
        请给我加上水印,谢谢
    </div>
    <script>
        /**
         * [createdWaterMask] 生成水印
         * @params {number} width 单个水印的宽度
         * @params {number} height 单个水印的高度
         * @params {number} content 水印内容
         * 
         * */
        function createdWaterMask(width, height, content) {
            // 生成水印节点
            const waterMaskBox = document.createElement("div")
            waterMaskBox.setAttribute("id", "waterMask")
            // 设置水印盒子的样式,其中user-select,pointer-events是关键
            waterMaskBox.style = `
                position: fixed;
                top: 0;
                bottom: 0;
                left: 0;
                range: 0;
                width: 100%;
                height: 100%;
                font-size: 18px; 
                font-weight: 600; 
                /* 控制水印dom布局  */
                display: flex;
                flex-wrap: wrap; 
                overflow: hidden;
                /* 整个水印盒子不能操作 */ 
                user-select: none; 
                pointer-events: none; 
                opacity: 0.3; 
                z-index: 9999;
            `
            // 把水印盒子插到body下
            document.body.appendChild(waterMaskBox)
            // 获取盒子的大小
            let box = document.getElementById("waterMask"); 
            let boxWidth = box.clientWidth, 
                boxHeight = box.clientHeight; 
            // 根据水印大小和水印盒子的大小(这里是视口大小),生成对应数量的dom元素,并插入
            for (let i = 0; i < Math.floor(boxHeight / height); i++) { 
                for (let j = 0; j < Math.floor(boxWidth / width); j++) { 
                    let next = document.createElement("div") 
                    next.style.width = width + 'px' 
                    next.style.height = height + 'px' 
                    next.innerText = content 
                    box.appendChild(next) 
                } 
            }
        }
        createdWaterMask(200, 200, '我是水印')
    </script>   
</body>
</html>

水印效果是实现了,但是由于该方案需要用js计算创建dom元素的数量和频繁创建,在视口修改的时候需要重复计算和操作水印的dom元素,在交互和性能上存在问题(切图仔也有追求,优雅永不过时),因此开始寻找其他方案。

方案2:通过canvas生成图片设置为背景

同理,第一步都是在body下插入一个fixed定位的div盒子并设置点透的css:pointer-events: none;,然后就是通过canvas绘画出单一的水印区域,并将该区域的内容转换成可以作为background-image值的base64格式的数据流,将该数据流设置为盒子的背景图,通过backgroud-repeat:repeat;样式完成整个屏幕的效果循环填充。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>canvas 生成水印</title>
</head>
<style>
</style>
<body>
    <div class="app">
        我是网页
    </div>
    <script>
        /**
         * [createdCanvas] 通过canvas生成单个水印图片
         * @params {number} width 单个水印的宽度
         * @params {number} height 单个水印的高度
         * @params {number} content 水印内容
         * 
         **/
        function createdCanvas(width, height, content) {
            const canvas = document.createElement('canvas')
            // 设置单个水印的宽高
            canvas.width = width
            canvas.height = height
            const ctx = canvas.getContext('2d')
            // 设置canvas文本样式
            ctx.font = '20px Microsoft Yahei';
            ctx.fillStyle = 'rgba(184, 184, 184, 0.6)';
            // 写入水印文案
            ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
            const img = canvas.toDataURL()
            return img
        }
        /**
         * [createdWaterMask] 生成水印
         * @params {number} width 单个水印的宽度
         * @params {number} height 单个水印的高度
         * @params {number} content 水印内容
         * 
         * */
        function createdWaterMask(width, height, content) {
            // 生成水印节点
            const waterMaskBox = document.createElement("div")
            waterMaskBox.setAttribute("id", "waterMask")
            // 获取对应的水印图片
            const base64Url = createdCanvas(width, height, content)
            waterMaskBox.style = `
                position: fixed;
                top: 0;
                bottom: 0;
                left: 0;
                range: 0;
                width: 100%;
                height: 100%;
                font-size: 18px; 
                font-weight: 600; 
                /* 控制水印dom布局  */
                display: flex;
                flex-wrap: wrap; 
                overflow: hidden;
                /* 整个水印盒子不能操作 */ 
                user-select: none; 
                pointer-events: none; 
                opacity: 1; 
                z-index: 9999;
                pointer-events:none;
                /*设置背景平铺和背景图*/
                background-repeat:repeat;
                background-image:url('${base64Url}');
            `
            // 把水印盒子插到body下
            document.body.appendChild(waterMaskBox)
        }
        createdWaterMask(300, 300, '我是水印')
    </script>   
</body>
</html>

该方案也是目前实现网页水印最常用的方法之一,不管是水印文案和水印的显示,都能灵活的通过参数来实现。

方案3:利用svg作为背景实现

和canvas的方案基本上相似,唯一的区别就是水印div的背景图不是直接生成的了,而是直接用svg来代替。在兼容性上而言,svg的兼容性会比vanvas的差一点。

Canvas兼容性图:

在这里插入图片描述

SVG兼容性图:

在这里插入图片描述

实现方案如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>canvas 生成水印</title>
</head>
<style>
</style>

<body>
    <div class="app">
        我是网页
    </div>
    <script>
        /**
         * [createdWaterMask] 生成水印
         * @params {number} width 单个水印的宽度
         * @params {number} height 单个水印的高度
         * @params {number} content 水印内容
         * 
         * */
        function createdWaterMask(width, height, content) {
            // 生成水印节点
            const waterMaskBox = document.createElement("div")
            waterMaskBox.setAttribute("id", "waterMask")
            // 获取对应的水印图片
            const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}">
                  <text x="50%" y="50%" dy="12px"
                    text-anchor="middle"
                    stroke="#000000"
                    stroke-width="1"
                    stroke-opacity="0.3"
                    fill="none"
                    transform="rotate(-30, 120 120)">
                    ${content}
                  </text>
                </svg>`;
            const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;


            waterMaskBox.style = `
                position: fixed;
                top: 0;
                bottom: 0;
                left: 0;
                range: 0;
                width: 100%;
                height: 100%;
                font-size: 18px; 
                font-weight: 600; 
                /* 控制水印dom布局  */
                display: flex;
                flex-wrap: wrap; 
                overflow: hidden;
                /* 整个水印盒子不能操作 */ 
                user-select: none; 
                pointer-events: none; 
                opacity: 1; 
                z-index: 9999;
                pointer-events:none;
                /*设置背景平铺和背景图*/
                background-repeat:repeat;
                background-image:url('${base64Url}');
            `
            // 把水印盒子插到body下
            document.body.appendChild(waterMaskBox)
        }
        createdWaterMask(300, 300, '我是水印')
    </script>
</body>

</html>

三、前端水印防护方案

在网页中添加水印是一项常见的版权保护措施。尽管目前有多种在前端生成并插入DOM元素的方法来实现水印功能,但它们都存在一个共同的弱点:对于具备一定前端知识的人来说,只需简单地查找并删除或隐藏页面中的相关DOM元素,便可轻松移除水印。为了克服这一缺陷,我们需要对页面进行实时监控。在这里,现代浏览器提供的MutationObserver API成为了一个理想的解决方案。它能够监控DOM树的任何变动,从而有效防止未经授权的DOM元素修改。作为旧的Mutation Events的替代品,MutationObserver是DOM3 Events规范的一部分,它为前端开发者提供了一种强大的手段来保护网页内容不被非法篡改。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>canvas 生成水印</title>
</head>
<style>
</style>

<body>
    <div class="app">
        我是网页
    </div>
    <script>
        /**
         * [createdCanvas] 通过canvas生成单个水印图片
         * @params {number} width 单个水印的宽度
         * @params {number} height 单个水印的高度
         * @params {number} content 水印内容
         * 
         **/
        function createdCanvas(width, height, content) {
            const canvas = document.createElement('canvas')
            // 设置单个水印的宽高
            canvas.width = width
            canvas.height = height
            const ctx = canvas.getContext('2d')
            // 设置canvas文本样式
            ctx.font = '20px Microsoft Yahei';
            ctx.fillStyle = 'rgba(184, 184, 184, 0.6)';
            // 写入水印文案
            ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
            const img = canvas.toDataURL()
            return img
        }
        /**
         * [createdWaterMask] 生成水印
         * @params {number} width 单个水印的宽度
         * @params {number} height 单个水印的高度
         * @params {number} content 水印内容
         * 
         * */
        function createdWaterMask(width, height, content) {
            // 生成水印节点
            const waterMaskBox = document.createElement("div")
            waterMaskBox.setAttribute("id", "waterMask")
            // 获取对应的水印图片
            const base64Url = createdCanvas(width, height, content)
            waterMaskBox.style = `
                position: fixed;
                top: 0;
                bottom: 0;
                left: 0;
                range: 0;
                width: 100%;
                height: 100%;
                border: 1px red solid;
                font-size: 18px; 
                font-weight: 600; 
                /* 控制水印dom布局  */
                display: flex;
                flex-wrap: wrap; 
                overflow: hidden;
                /* 整个水印盒子不能操作 */ 
                user-select: none; 
                pointer-events: none; 
                opacity: 1; 
                z-index: 9999;
                pointer-events:none;
                /*设置背景平铺和背景图*/
                background-repeat:repeat;
                background-image:url('${base64Url}');
            `
            // 把水印盒子插到body下
            document.body.appendChild(waterMaskBox)
        }
        createdWaterMask(300, 300, '我是水印')

        // 记录水印的当前的节点信息
        const firstWaterMaskEl = document.getElementById('waterMask')
        // 水印节点样式
        const firstWaterMaskStyle = firstWaterMaskEl?.getAttribute('style')

        const observer = new MutationObserver(() => {
            // 获取水印节点
            const instance = document.getElementById('waterMask')
            // 水印节点样式
            const style = instance?.getAttribute(' style')
            // 修改样式删除div
            if ((instance && style !== firstWaterMaskStyle) || !instance) {
                // 避免重复触发监听,这里先暂停
                observer.disconnect()
                if (instance) {
                    instance.setAttribute('style', firstWaterMaskStyle)
                } else {
                    // div不在,说明删除了div
                    createdWaterMask(300, 300, '我是水印')
                }
                // 修改完重新开启监听
                 observer.observe(document.body, {
                    childList: true,
                    attributes: true,
                    subtree: true,
                })
            }
        })

        // 启动监控
        observer.observe(document.body, {
            childList: true,
            attributes: true,
            subtree: true,
        })

    </script>
</body>

</html>

到此就实现了网页水印和对水印节点的监听,但是即使如此也只是相对安全,毕竟这些数据和操作都是在客户端上实现,还是有很多方法可以绕过。只能防君子不能防小人。

作者介绍:

道一云七巧低代码开发平台,让零代码人员也能轻松构建企业级应用。通过可视化拖拽和模型驱动,实现快速开发和部署,加速企业信息化进程。利用道一云七巧低代码平台,企业可以快速实现个性化应用开发,规范流程管理,显著提升团队协作效率。作为高生产力aPaaS平台,道一云七巧是企业数字化转型的理想选择。

欢迎关注:
官网:道一云七巧 - 可视化、智能化、数字化应用构建
免费体验:道一云产品免费试用
公众号:道一云低代码(do1info)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Presto报错:[Presto requires an Oracle or OpenJDK JVM (found Red Hat, Inc.)]

启动前: 已经搭建了jdk环境hadoop的jdk环境配置没有问题 启动Presto时&#xff0c;报错 解决方案: 可能是presto自身变量配置没生效在presto路径下找到bin目录, 进入启动脚本launcher 在 exec "$(dirname “ 0 " ) / l a u n c h e r . p y " " 0"…

格式工厂转换视频分辨率

1、下载和安装 http://www.pcfreetime.com/formatfactory/CN/index.html 2、打开视频 3、设置分辨率等参数 也可以选择保持原分辨率 4、执行导出 5、打开输出所在位置

nvm 管理多版本 node

1、下载 先不安装node 下载 nvm 1.1.10-setup.zip 解压&#xff1a;nvm&#xff1a;https://nvm.uihtm.com/ 新建nodejs/node、nodejs/nvm文件夹用于存放node版本和nvm安装路径 安装nvm&#xff1a;上述链接有安装教程 查看是否安装成功&#xff1a;重新打开cmd 输入 nvm nv…

SenseVoice - 阿里最新开源精准多语言语音识别与情感辨识模型 本地一键整合包下载

阿里巴巴近期发布了开源语音大模型项目FunAudioLLM&#xff0c;该项目包含了两个核心模型&#xff1a;SenseVoice和CosyVoice。可以精准多语言识别并且进行语音克隆 本地一键包下载地址&#xff1a; SenseVoice - 精准多语言语音识别与情感辨识模型 本地一键整合包下载 SenseVo…

本地部署 SenseVoice - 阿里开源语音大模型

本地部署 SenseVoice - 阿里开源语音大模型 1. 创建虚拟环境2. 克隆代码3. 安装依赖模块4. 启动 WebUI5. 访问 WebUI 1. 创建虚拟环境 conda create -n sensevoice python3.11 -y conda activate sensevoice 2. 克隆代码 git clone https://github.com/FunAudioLLM/SenseVoic…

二阶线性微分方程

假设一个质量 m 连接在弹簧和阻尼器上&#xff0c;系统受到外力 F(t) 的作用。设 x(t) 为质量的位移&#xff0c;系统的运动方程可以用牛顿第二定律表示为&#xff1a; 这是一个典型的二阶线性非齐次微分方程&#xff1a;其中&#xff1a; m 是质量&#xff08;Fma&#xff09…

“El-Table二次封装“这样做【高级前端必备技能之一】

&#x1f525; 前言 这篇文章给大家分享一个高级自定义列表组件从0到1的开发过程&#xff0c;这个列表组件的主要功能有&#xff0c;列表拖拽排序&#xff0c;右侧操作按钮统一使用Tooltip展示&#xff0c;操作表头增加自定列表icon&#xff0c;点击icon可以对列表展示数据进行…

玩具营销是如何拿捏成年人钱包?

好像现在的成年人逐渐热衷于偏向年轻化&#xff0c;问问题会好奇“尊嘟假嘟”&#xff0c;饭量上的“儿童套餐”&#xff0c;娃娃机前排长队......而最突出的莫过于各类各式的玩具不断收割当代年轻人&#xff0c;除去常给大朋友们小朋友们送去玩具福利的“麦、肯”双门&#xf…

nvm安装使用 nrm使用

因维护老项目及开发新项目同时进行&#xff0c;需要使用不同版本的node进行运行&#xff0c;所以用nvm进行多个版本的node维护&#xff0c;通过nrm进行镜像源管理切换 简介 Node.js 是一种基于 Chrome V8 引擎的 JavaScript 运行环境&#xff0c;用于构建高性能的网络应用程序…

Linux--线程的控制

目录 0.前言 1.pthread库 2.关于控制线程的接口 2.1.创建线程&#xff08;pthread_create&#xff09; 2.2.线程等待&#xff08;pthread_join&#xff09; 代码示例1&#xff1a; ​编辑 ***一些问题*** 2. 3.创建多线程 3.线程的终止 &#xff08;pthread_exit /…

用LangGraph、 Ollama,构建个人的 AI Agent

如果你还记得今年的 Google I/O大会&#xff0c;你肯定注意到了他们今年发布的 Astra&#xff0c;一个人工智能体&#xff08;AI Agent&#xff09;。事实上&#xff0c;目前最新的 GPT-4o 也是个 AI Agent。 现在各大科技公司正在投入巨额资金来创建人工智能体&#xff08;AI …

逻辑回归模型(非回归问题,而是解决二分类问题)

目录&#xff1a; 一、Sigmoid激活函数&#xff1a;二、逻辑回归介绍&#xff1a;三、决策边界四、逻辑回归模型训练过程&#xff1a;1.训练目标&#xff1a;2.梯度下降调整参数&#xff1a; 一、Sigmoid激活函数&#xff1a; Sigmoid函数是构建逻辑回归模型的重要激活函数&am…

C++ | Leetcode C++题解之第221题最大正方形

题目&#xff1a; 题解&#xff1a; class Solution { public:int maximalSquare(vector<vector<char>>& matrix) {if (matrix.size() 0 || matrix[0].size() 0) {return 0;}int maxSide 0;int rows matrix.size(), columns matrix[0].size();vector<…

Java基础语法--基本数据类型

Java基础语法–基本数据类型 Java是一种静态类型语言&#xff0c;这意味着每个变量在使用前都必须声明其数据类型。Java提供了多种基本数据类型&#xff0c;用于存储整数、浮点数、字符和布尔值等。以下是Java中的基本数据类型及其特点&#xff1a; 1. 整型&#xff08;Integ…

Java面试八股之MySQL中的MVCC是什么,作用是什么?

MySQL中的MVCC是什么&#xff0c;作用是什么&#xff1f; MySQL中的MVCC&#xff08;Multiversion Concurrency Control&#xff0c;多版本并发控制&#xff09;是一种并发控制机制&#xff0c;用于提高数据库的并发性能并确保数据的一致性&#xff0c;特别是在高并发读写场景…

python调用qt编写的dll

报错&#xff1a;FileNotFoundError: Could not find module F:\pythonProject\MINGW\sgp4Lib.dll (or one of its dependencies). Try using the full path with constructor syntax. 只有两种情况&#xff1a; 1.路径不对 2.库的依赖不全 1、如果是使用了qt库的&#xff0…

使用tkinter拖入excel文件并显示

使用tkinter拖入excel文件并显示 效果代码 效果 代码 import tkinter as tk from tkinter import ttk from tkinterdnd2 import TkinterDnD, DND_FILES import pandas as pdclass ExcelViewerApp(TkinterDnD.Tk):def __init__(self):super().__init__()self.title("Excel…

unity 手动制作天空盒及使用

提示&#xff1a;文章有错误的地方&#xff0c;还望诸位大神不吝指教&#xff01; 文章目录 前言一、使用前后左右上下六张图1.准备6张机密结合的图片2.创建Material材质球3.使用天空盒 二、使用HDR贴图制作1.准备HDR贴图2.导入unity 修改Texture Sourpe 属性3.创建材质球4.使用…

自定义刷题工具-python实现

背景&#xff1a; 最近想要刷题&#xff0c;虽然目前有很多成熟的软件&#xff0c;网站。但是能够支持自定义的导入题库的非常少&#xff0c;或者是要么让你开会员&#xff0c;而直接百度题库的话&#xff0c;正确答案就摆在你一眼能看见的地方&#xff0c;看的时候总觉得自己…

使用Python绘制QQ图并分析数据

使用Python绘制QQ图并分析数据 在这篇博客中&#xff0c;我们将探讨如何使用Python中的pandas库和matplotlib库来绘制QQ图&#xff08;Quantile-Quantile Plot&#xff09;&#xff0c;并分析数据文件中的内容。QQ图是一种常用的统计图表&#xff0c;用于检查一组数据是否服从…