前端截图身份溯源

638次阅读  |  发布于2年以前

背景

今天在跟同事朋友聊这个脉脉上的一些事,我就突然想到之前很多人喜欢截图各种APP页面发一些比较敏感的内容出来,于是就写了这篇文章

开头

从今年看到这个新闻开始,我就开始警觉了。

先回到技术

前端有一种东西叫水印,特别当你浏览一些内部系统的时候,大部分都会有,例如现在的企业微信,担心你截图发出去内部的重要信息。

小伙伴们会发现,水印大部分都是我们使用者可以看见的,例如企业微信的,这种我们叫明水印

可是还有一种,叫暗水印

什么叫暗水印?

就是你肉眼一眼看不到的水印,不影响你的浏览页面内容,无感知的把水印加进去。

这样一些敏感的内容,一旦泄露出去,就能通过网上的图片,解码找出来是谁的身份泄漏出去,而且往往一般的泄漏者不知道原来会有这个东西

所以有人会出现,截个图发给朋友,然后网上疯传,最后被定位到是他发的图片。这就是暗水印的一种使用

所以大家不要乱截图APP和各种页面,特别是比较敏感的内容

最简单的明水印实现

使用canvas生成,然后使用backgroud-repea至需要展示水印的区域~

 function __canvasWM({
    container = document.body,
    width = '250px',
    height = '200px',
    textAlign = 'center',
    textBaseline = 'middle',
    font = '15px microsoft yahei',
    fillStyle = 'rgba(184, 184, 184, 0.8)',
    content = '请勿外传',
    rotate = '30',
}) {
    const canvas = document.createElement('canvas');

    canvas.setAttribute('width', width);
    canvas.setAttribute('height', height);
    const ctx = canvas.getContext('2d');
    ctx.textAlign = textAlign;
    ctx.textBaseline = textBaseline;
    ctx.font = font;
    ctx.fillStyle = fillStyle;
    ctx.rotate((Math.PI / 180) * rotate);
    ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
    const base = canvas.toDataURL();
    const watermarkDiv = document.createElement('div');
    watermarkDiv.setAttribute(
        'style',
        `
          position:fixed;
          top:0;
          left:0;
          width:100%;
          height:100%;
          z-index:${9999999999};
          pointer-events:none;
          background-repeat:repeat;
          background-image:url('${base}');
          color:grey;
          opacity: 0.5`
    );

    container.style.position = 'relative';
    container.insertBefore(watermarkDiv, container.firstChild);
}

明水印的缺点

容易被找到对应的dom元素然后delete,我们有两种方式防止被删除:

1.setInterval去不断检测水印,如果不在了就继续生成

2.使用MutationObserver监测dom的变化,如果水印的dom节点变化了,就要继续生成

但是前端安全是一个比较麻烦的事,还是可以在控制台禁用javascript来做到去除的目的。

明水印肯定还有其他实现和防止去除的手段,安全是一个很深的知识点,这里点到为止

暗水印实现

同样,暗水印的实现,是多种的。我看过网上就有很多种,这里简单举两个例子

例如:https://juejin.cn/post/7023712210899697671这篇文章里面提到的

即在页面最顶部加上透明蒙层,蒙层携带文字信息,文字颜色也为透明,截图后文字信息肉眼不可见,通过ps等图片处理工具处理截图,才会将文字信息展示出来

通过PS设置,将看不见的水印内容展示出来

还有一种是业内通用的方案:

图片是由多个像素点组成的,而每个像素点轻微的变化,肉眼是无法辨别的

网上挺多这种文章的,我就从https://juejin.cn/post/7064201037321601032搞点代码过来了。时间原因,不自己写了

代码:

function createBackgroundImage(content, proportion, tiltAngle) {
  const can = document.createElement("canvas");
  can.width = document.body.clientWidth / proportion;
  can.height = document.body.clientHeight / proportion;
  const context = can.getContext("2d");
  context.rotate((-25 * Math.PI) / 180);
  context.font = "800 30px Microsoft JhengHei";
  context.fillStyle = "#000";
  context.textAlign = "center";
  context.textBaseline = "Middle";
  context.fillText(content, 100, 100);
  console.log(context.getImageData(0, 0, can.width, can.height));
  return can.toDataURL("image/png");
}

   const div = document.getElementById("root");
    div.style.backgroundImage = `url(${createBackgroundImage(
      "前端巅峰",
      6,
      10
    )})`;

效果:

其实这个效果背后是,root节点给了一个背景图:

那么我们获取下这个背景图的信息看看

console.log((createBackgroundImage("前端巅峰", 6, 10)));

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

red=imgData.data[0];
green=imgData.data[1];
blue=imgData.data[2];
alpha=imgData.data[3];

Uint8ClampedArray则是对应的像素点,每四个表征一个像素点,然后从左往右,从上往下的顺序进行排列

我们只需要把水印内容塞进图片中即可


function getImageData(image) {
  const img = new Image();
  img.src = image;
  const myCanvas = document.createElement("canvas");
  myCanvas.width = img.width;
  myCanvas.height = img.height;
  const myContext = myCanvas.getContext("2d");
  myContext.drawImage(img, 0, 0);
  return myContext.getImageData(0, 0, myCanvas.width, myCanvas.height);
}

function mergeData(rawImageSrc, watermarkImageSrc) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function () {
      const myCanvas = document.createElement("canvas");
      myCanvas.width = img.width;
      myCanvas.height = img.height;
      const ctx = myCanvas.getContext("2d");
      const bit = 0;
      const offset = 3;
      const oImageData = getImageData(rawImageSrc);
      const oData = oImageData.data;
      const newData = getImageData(watermarkImageSrc).data;
      for (let i = 0; i < oData.length; i++) {
        if (i % 4 === bit) {
          // 只修改目标通道
          if (newData[i + offset] === 0 && oData[i] % 2 === 1) {
            // 没有信息的像素,将目标通道的奇数像素改为偶数
            if (oData[i] === 255) {
              oData[i]--;
            } else {
              oData[i]++;
            }
          } else if (newData[i + offset] !== 0 && oData[i] % 2 === 0) {
            // 有信息的像素
            oData[i]++;
          }
        }
      }
      ctx.putImageData(oImageData, 0, 0);
      resolve(myCanvas.toDataURL("image/png"));
    };
    img.src = rawImageSrc;
  });
}

然后把水印内容从图片抽取出来:

function decrypt(watermarkImage) {
 return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = function () {

          const myCanvas = document.createElement("canvas");
          myCanvas.width = img.width;
          myCanvas.height = img.height;
          const ctx = myCanvas.getContext("2d")


          const imageData = getImageData(watermarkImage)
          var data = imageData.data;
          for (var i = 0; i < data.length; i++) {
            if (i % 4 == 0) {
              // 红色分量
              if (data[i] % 2 == 0) {
                data[i] = 0;
              } else {
                data[i] = 255;
              }
            } else if (i % 4 == 3) {
              // alpha通道不做处理
              continue;
            } else {
              // 关闭其他分量,不关闭也不影响答案,甚至更美观 o(^▽^)o
              data[i] = 0;
            }
          }

          ctx.putImageData(imageData, 0, 0)
          resolve(myCanvas.toDataURL("image/png"))
        }

        img.src = watermarkImage
      })
}

最终使用:

    const image1 = createBackgroundImage('前端巅峰', 3, 10)
    const image2 = createBackgroundImage('Peter666', 3, 10)
    mergeData(image1, image2).then(res => {
      console.log('res', res)
      decrypt(image2).then(res => {
        console.log('finalImage', res)
      })
    })

多说无益

我使用webContainer技术的给大家生成了一个demo

项目源代码链接:https://stackblitz.com/edit/react-a9ehdm?file=src/App.js

预览链接(打开控制台可以看到效果):https://react-a9ehdm.stackblitz.io

stackblitz上面可能会有报错, 大家可以把代码搞到本地跑起来

总结

所以大家没事不要乱截图,如果是敏感内容,暗水印实现方式很多种方式,总有一种你不知道的,会找到你。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8