百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

「前端添加水印」你真的了解全面吗?

toyiye 2024-07-02 03:01 14 浏览 0 评论

背景

在古茗日常业务中,经常会给加盟商下发各种资料,例如:奶茶的配方、设备的清洗、卫生的标准等等等。这些资料都是一些内部资料,从信息安全维度不能被泄露和盗取出去。所以会给下发的资料加上水印。这些资料可能是纯文本,也可能是文本加图片的。因此,我们要做好以下两个方面:

  • 通过对页面增加水印,可以从系统级别防止别人盗取我们的页面信息
  • 通过对单独的图片加水印 - 防止图片保存时没有水印

页面水印

方案设计

实现页面水印的方式有很多,可以看一些常用页面加水印的方案,具体如下:

  • 方案一:fixed 定位的 div 元素,重复渲染 div 元素来添加水印。会创建很多无关的 DOM 元素
  • 方案二:fixed 定位 canvas 元素,重复填充水印。始终会创建一个无关的 canvas 元素
  • 方案三:canvas + 伪类。不会创建无关元素,且兼容性好
  • 方案四:svg + 伪类。不会创建无关元素,但兼容性略差于 canvas

这些方案,都有一个通用的缺点,那就是将元素删掉,或者将类名删掉,都能去除页面水印。

基于实现成本和安全性维度的考虑,最终方案选型:方案三,同时增加了通过MutationObserver - Web API 接口参考 | MDN 解决了删除类名导致水印删除的问题。

核心功能点:

  • 把签名信息,通过 Canvas 生成背景图
  • 利用伪类将背景图添加到需要生成水印的区域上
  • 通过 MutationObserver , 解决了删除类名导致水印删除的问题

代码实现

把签名信息,通过Canvas生成背景图

  • 利用 Canvas 来绘制背景图,背景内容为水印的内容
  • 通过 toDataURL 将 Canvas 转换成图片,格式为 image/png
interface IImgOptions {
  content: string[]; // 水印的内容,可传递多个水印
  canvasHeight: number; // 画布的高度
  canvasWidth: number; // 画布的宽度
}
const createImgBase = (options: IImgOptions) => {
  const { content, canvasHeight, canvasWidth } = options;
  const canvas = document.createElement('canvas'); // 创建一个画布
  const ctx = canvas.getContext('2d');
  // 设置画布的宽高
  canvas.width = canvasHeight;
  canvas.height = canvasWidth;
  if (ctx) {
    ctx.rotate((-10 * Math.PI) / 180); // 偏移一点距离
    ctx.fillStyle = 'rgba(100,100,100,0.2)'; // 设置绘制的颜色
    ctx.font = '40px'; // 设置字体的大小
    // 遍历水印内容
    content.forEach((text, index) => {
      ctx.fillText(text, 10, 30 * (index + 1)); // 拉开30的间距
    });
  }
  return canvas.toDataURL('image/png'); // 转换程data url,可供img直接使用
};

利用伪类将背景图添加到整个页面上

  • 给需要添加水印元素添加一个对应的伪元素,将第一步通过 Canvas 生成的 data url 作为背景
  • 创建一个 style 元素,将伪元素放在 style.innerHTML 中,然后 appendChild 到 head 中,此时,页面水印就加完了
const genWaterMark = ({
  content,
  className,
  canvasHeight = 140,
  canvasWidth = 150,
}) => {
  const dataURL = createImgBase({ content, canvasHeight, canvasWidth });
  const defaultStyle = document.createElement('style');
  defaultStyle.innerHTML = `.${className}::after {
    content: '';
    display: block;
    width: 100%;
    height: 100vh;
    background-image: url(${dataURL});
    background-repeat: repeat;
    pointer-events: none;
    position: fixed;
    top: 0;
    left: 0;
  }`;
  document.head.appendChild(defaultStyle);
};


// 使用方式
const Content = () => {
    useEffect(() => {
    genWaterMark({
      content: [userName, userPhone, '内部机密材料', '严禁外泄!'],
      className: 'my-page-container',
    });
  }, []);
  return (
    <div className="my-page-container" id="my-page-container">
      <div className="my-info">
        <div className="title">这是测试标题</div>
        <div className="content">
          // ...我想这是机密内容 * n
        </div>
      </div>
    </div>
  )
}

// css样式
.my-page-container {
  height: calc(100vh - 104px);
  overflow: hidden;

  .my-info {
    display: flex;
    flex: 1;
    flex-direction: column;
    height: 100%;
    padding: 24px;
    overflow-y: auto;

    // .title & .content 一些不重要的css
  }

页面效果如下:脱敏处理,截图未展示姓名和手机号。

利用MutationObserver,防止被人删除className

const listenerDOMChange = (className: string) => {
  const targetNode = document.querySelector(`.${className}`);
  const observer = new MutationObserver((mutationsList) => {
    for (let mutation of mutationsList) {
      if (mutation.type === 'attributes' && mutation.attributeName === 'class' && targetNode) { // 监听属性并且属性名为class的变更
        const curClassVal = targetNode.getAttribute('class') || '';
        if (curClassVal.indexOf(className) === -1) { // 监听到className被删除了,手动加回去
          targetNode.setAttribute('class', `${className} ${curClassVal}`);
        }
      }
    }
  });

  observer.observe(targetNode as Node, {
    attributes: true,
  });
};
const genWaterMark = ({
  content,
  className,
  canvasHeight = 140,
  canvasWidth = 150,
}: IWaterMark) => {
  // 监听class的变更
  listenerDOMChange(className);
  const dataURL = createImgBase({ content, canvasHeight, canvasWidth });
  const defaultStyle = document.createElement('style');
  // 省略
  document.head.appendChild(defaultStyle);
};

注意点

注意点①:

  • 问题:
  • 上述方案的水印是占据整个页面的,但有些水印期望是在特定区域的。
  • 解决方案:
  • 利用定位,实现在特定区域增加水印
// 通过设置position: absolute来实现
const genWaterMark = ({
  content,
  className,
  canvasHeight = 140,
  canvasWidth = 150,
}: IWaterMark) => {
  const dataURL = createImgBase({ content, canvasHeight, canvasWidth });
  const defaultStyle = document.createElement('style');
  defaultStyle.innerHTML = `.${className}::after {
    content: '';
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background-image: url(${dataURL});
    background-repeat: repeat;
    pointer-events: none;
  }`;
  document.head.appendChild(defaultStyle);
};

// 使用方式
const Content = () => {
  useEffect(() => {
    genWaterMark({
      content: [userName, userPhone, '内部机密材料', '严禁外泄!'],
      className: 'wait-task-wrap',
    });
  }, []);
  return (
    <View className="my-page-container" id="my-page-container">
      // ...一些不重要的代码
      <View className="wait-task-wrap"></View>
    </View>
  )
}

// css样式
.wait-task-wrap {
  // 一些不重要的样式
  position: relative;
}

页面效果如下:

3、图片水印

3.1 方案设计

在资料中,存在很多图片,但页面水印,对图片你来说就我们要对图片进行预览并且支持保存。此时页面背景水印就没有用啦,我们下载下来的图片还是不带水印的。针对这种现象,我们有以下一些常用的解决方案

  • 方案一:服务端添加水印,安全,但是服务端压力大且性能慢
  • 方案二:借助 oss 添加水印,简便但是不通用
  • 方案三:canvas 方案,安全但性能慢

本文着重介绍后两种前端添加水印的方式。

代码实现

借助oss

将oss地址转成带水印的oss地址

// oss水印中的文字进行url安全的base64编码
const getSafeBase64Code = (name: string) => {
  return window
    .btoa(unescape(encodeURIComponent(name)))
    .replace(/+/g, '-')
    .replace(//+/g, '_');
};

const genOSSImageWaterMark = (imgSrc: string) => {
  const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');

  return `${imgSrc}?x-oss-process=image/watermark,text_${getSafeBase64Code(
    `${userName}-${userPhone}`
  )},rotate_325,t_10,color_000000,size_30,fill_1,g_nw,x_30,y_30`;
};

// 使用
const ImageWaterMark = () => {
  return (
    <Image
      src={genOSSImageWaterMark('xxx图片地址xxx')}
    />
  )
}

页面效果如下:

注意点

注意点①

  • 问题:有些图片某些区域是透明的,导致透明的区域上不了色。(效果如图一)
  • 解决方案:ui 告诉我们,png 图片导出默认是透明的,但是 jpg 默认会将透明的地方填充白色的背景,所以,我们查阅对图片进行格式转换的参数说明及实例_对象存储-阿里云帮助中心文档得出,只需要加上 x-oss-process=image/format,jpg, 对之前的 genOSSImageWaterMark 进行改造,对非 jpg 的图片都转成 jpg 的图片
const genOSSImageWaterMark = (imgSrc: string) => {
  const imgType = imgSrc.split('.').slice(-1)[0];
  const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');

  return `${imgSrc}?${
    imgType !== 'jpg' ? 'x-oss-process=image/format,jpg,' : ''
  }x-oss-process=image/watermark,text_${getSafeBase64Code(
    `${userName}-${userPhone}`
  )},rotate_325,t_10,color_000000,size_30,fill_1,g_nw,x_30,y_30`;
};

效果如下:

注意点②

  • 问题:字体写死,导致水印在大图上特别小,小图上特别大。(效果如图二)
  • 解决方案:根据图片比,计算字体大小。
interface IImageProps {
  width: number;
  height: number;
}
// 获取图片的宽高
const getImageWH = async (src): Promise<IImageProps> => {
  const img = new Image();
  img.src = src;
  await new Promise((resolve) => (img.onload = resolve));  // 等图片加载完
  return new Promise((resolve) => {
    resolve({
      width: img.width,
      height: img.height,
    });
  });
};
const genOSSImageWaterMark = async (imgSrc: string) => {
  const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');
  const imgType = imgSrc.split('.').slice(-1)[0];
  const { width, height } = await getImageWH(imgSrc);
  const min = Math.min(width, height);
  // 根据官网上的测试图片,宽度为400,设置字体为10,水印展示效果很好,所以图片比为40
  const size = Math.ceil(min / 40);
  const src = `${imgSrc}?${
    imgType !== 'jpg' ? 'x-oss-process=image/format,jpg,' : ''
  }x-oss-process=image/watermark,text_${getSafeBase64Code(
    `${userName}-${userPhone}`
  )},rotate_325,t_100,color_ff0000,size_${size},fill_1,g_nw,x_30,y_30`;
  return src;
};

效果如下:

注意点③

  • 问题:
  • 用户直接将后缀删了,水印也就没了
  • 解决方式:
  • oss 设置安全级别,不带水印不可访问

通过canvas给图片增加水印

技术方案设计

  • 图片路径转成 canvas
  • canvas 添加水印
  • canvas 转成 img

代码实现

const genOSSImageWaterMark = async (imgSrc: string) => {
  const canvas = document.createElement('canvas');
  // ① 图片路径转成canvas
  await imgSrc2Canvas(canvas, imgSrc);
  // ② canvas添加水印
  addWatermark(canvas);

  // ③ canvas转成img
  return canvas.toDataURL('image/png');
};

使用

const genOSSImageWaterMark = async (imgSrc: string) => {
  const canvas = document.createElement('canvas');
  await imgSrc2Canvas(canvas, imgSrc);
  addWatermark(canvas);

  return canvas.toDataURL('image/png');
};

图片路径转成canvas

const imgSrc2Canvas = (cav: HTMLCanvasElement, imgSrc: string) => {
  return new Promise(async (resolve) => {
    const image = new Image();
    image.src = imgSrc;
    // ① 为图片设置crossOrigin属性,防止Failed to execute 'toDataURL' on 'HTMLCanvasElement'
    image.setAttribute('crossOrigin', 'anonymous');
    // ② 解决渲染图片为透明图层
    await new Promise((resolve) => (image.onload = resolve));
    cav.width = image.width;
    cav.height = image.height;
    const ctx = cav.getContext('2d');
    if (ctx) {
      ctx.drawImage(image, 0, 0);
    }
    resolve(cav);
  });
};

canvas添加文字水印

  • 通过二维数组的渲染,来填充文本
  • 通过画布的宽度以及水印的宽度来计算 X 轴的渲染次数通过画布的宽度以及你想打印的疏密程度来计算 Y 轴的渲染次数
const addWatermark = async (canvas, imgSrc) => {
  const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'rgba(100,100,100,0.2)'; // 字体颜色
  ctx.font = `24px serif`;
  ctx.translate(0, 0);
  ctx.rotate((5 * Math.PI) / 180); // 旋转角度

  const repeatX = Math.floor(canvas.width / 240); // 100 为每个水印的基本宽度
  const repeatY = Math.floor(canvas.height / 150);
  for (let i = 0; i < repeatX; i++) {
    for (let j = 1; j < repeatY; j++) {
      ctx.fillText(`${userName}-${userPhone}`, 240 * 2 * i, 150 * j); // 控制水印的疏密
    }
  }
};

页面效果如下:

注意点

注意点①:

  • 问题:页面报错如下
  • 原因:当 img 元素的 src 不符合同源准则时,会阻止读取 canvas 的内容。因为此时 img 元素放在 canvas 中时,canvas 元素会被标记为被污染的,而在被污染的 canvas 中调用 toDataUrl 将会报错
  • 解决方案:
// 为image设置crossOrigin属性
image.setAttribute('crossOrigin', 'anonymous');

注意点②:

  • 问题:渲染的图片为透明的图片
  • 原因:图片还未渲染完,就返回了 canvas。
  • 解决方案:等图片渲染完了,再开始画到 canvas 中
await new Promise((resolve) => (image.onload = resolve));

总结

本文主要讲了两个话题:页面水印 & 图片水印。页面水印很简单,基本上就是利用 canvas 渲染水印,再利用伪类将 canvas 的水印渲染在特定的区域。图片相对而言会复杂一些,在渲染水印之前,得先把图片渲染上去,针对大图,性能可能会慢一点。所以,如果对水印要求不是很严格并且图片是存储在 oss 的,那利用 oss 来加水印也不失为一种好选择。但如果从安全性来考虑,那肯定是服务端加水印会更合适一点。


原文链接:https://juejin.cn/post/7302724955699822631

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码