从破解某定设计网站谈前端水印(具体教程)

前言

最近在写公众号的时候,经常会本人做首图,并且缓缓地发现沉迷于制作首图,感觉扁平化的设计的真好好看。缓缓地萌发了一个做一个属于本人的首图生成器的想法。

制作呢,当然也不是拍拍脑袋就开始,在开始之前,就去钻研了一下某在线设计网站(如果有人不晓得的话,能够说一下,这是一个在线制作海报之类的网站 T T 像咱们这种内容创作者用的比拟多),毕竟人家曾经做了很久了,我只是想做个不便集体应用的。毕竟以上用 PS 做着还是有一些废工夫,因为组成的元素都很简略,做一个自动化生成的齐全能够。

然而钻研着钻研着,就看到了某在线设计网站的水印,像这种技术支持的网站,最重要的进攻措施就是水印了,水印可能很好的爱护知识产权。

缓缓地路就走偏了,开始对它的水印感兴趣了。不禁发现之前只是大略晓得水印的生成办法,然而素来没有认真钻研过,本文将以以下的路线进行解说。以下所有代码示例均在

https://github.com/hua1995116/node-demo/tree/master/watermark

明水印

水印(watermark)是一种容易辨认、被夹于纸内,可能透过光线穿过从而显现出各种不同暗影的技术。

水印的类型有很多,有一些是整图笼罩在图层上的水印,还有一些是在角落。

那么这个水印怎么实现呢?相熟 PS 的敌人,都晓得 PS 有个叫做图层的概念。

网页也是如此。咱们能够通过相对定位,来将水印笼罩到咱们的页面之上。

最终变成了这个样子。

等等,然而发现少了点什么。间接笼罩下来,就如同是一个蒙层,我都晓得这样是无奈触发底下图层的事件的,此时就要介绍一个css属性pointer-events

pointer-events CSS 属性指定在什么状况下 (如果有) 某个特定的图形元素能够成为鼠标事件的 target。

当它的被设置为 none 的时候,能让元素实体虚化,尽管存在这个元素,然而该元素不会触发鼠标事件。详情能够查看 CSS3 pointer-events:none利用举例及扩大 « 张鑫旭-鑫空间-鑫生存 。

这下理清了实现原理,等于胜利了一半了!

明水印的生成

明水印的生成形式次要能够归为两类,一种是 纯 html 元素(纯div),另一种则为背景图(canvas/svg)。

上面我别离来介绍一下,两种形式。

div实现

咱们首先来讲比较简单的 div 生成的形式。就依照咱们方才说的。

// 文本内容<div class="app">        <h1>秋风</h1>        <p>hello</p></div> 

首先咱们来生成一个水印块,就是下面的 一个个秋风的笔记。这里次要有一点就是设置一个透明度(为了让水印看起来不是那么显著,从而不遮挡咱们的次要页面),另一个就是一个旋转,如果是正的程度会显得不是那么难看,最初一点就是应用 userSelect属性,让此时的文字无奈被选中。

userSelect

CSS 属性 user-select 管制用户是否选中文本。除了文本框内,它对被载入为 chrome 的内容没有影响。
function cssHelper(el, prototype) {  for (let i in prototype) {    el.style[i] = prototype[i]  }}const item = document.createElement('div')item.innerHTML = '秋风的笔记'cssHelper(item, {  position: 'absolute',  top: `50px`,  left: `50px`,  fontSize: `16px`,  color: '#000',  lineHeight: 1.5,  opacity: 0.1,  transform: `rotate(-15deg)`,  transformOrigin: '0 0',  userSelect: 'none',  whiteSpace: 'nowrap',  overflow: 'hidden',}) 

有了一个水印片,咱们就能够通过计算屏幕的宽高,以及水印的大小来计算咱们须要生成的水印个数。

const waterHeight = 100;const waterWidth = 180;const { clientWidth, clientHeight } = document.documentElement || document.body;const column = Math.ceil(clientWidth / waterWidth);const rows = Math.ceil(clientHeight / waterHeight);for (let i = 0; i < column * rows; i++) {    const wrap = document.createElement('div');    cssHelper(wrap, Object.create({        position: 'relative',        width: `${waterWidth}px`,        height: `${waterHeight}px`,        flex: `0 0 ${waterWidth}px`,        overflow: 'hidden',    }));    wrap.appendChild(createItem());    waterWrapper.appendChild(wrap)}document.body.appendChild(waterWrapper) 

这样子咱们就完满地实现了下面咱们给出的思路的样子啦。

背景图实现

canvas

canvas的实现很简略,次要是利用canvas 绘制一个水印,而后将它转化为 base64 的图片,通过canvas.toDataURL() 来拿到文件流的 url ,对于文件流相干转化能够参考我之前写的文章一文带你层层解锁「文件下载」的神秘, 而后将获取的 url 填充在一个元素的背景中,而后咱们设置背景图片的属性为反复。

.watermark {    position: fixed;    top: 0px;    right: 0px;    bottom: 0px;    left: 0px;    pointer-events: none;    background-repeat: repeat;} 
function createWaterMark() {  const angle = -20;  const txt = '秋风的笔记'  const canvas = document.createElement('canvas');  canvas.width = 180;  canvas.height = 100;  const ctx = canvas.getContext('2d');  ctx.clearRect(0, 0, 180, 100);  ctx.fillStyle = '#000';  ctx.globalAlpha = 0.1;  ctx.font = `16px serif`  ctx.rotate(Math.PI / 180 * angle);  ctx.fillText(txt, 0, 50);  return canvas.toDataURL();}const watermakr = document.createElement('div');watermakr.className = 'watermark';watermakr.style.backgroundImage = `url(${createWaterMark()})`document.body.appendChild(watermakr); 
svg

svg 和 canvas 相似,次要还是生成背景图片。

function createWaterMark() {  const svgStr =    `<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">      <text x="0px" y="30px" dy="16px"      text-anchor="start"      stroke="#000"      stroke-opacity="0.1"      fill="none"      transform="rotate(-20)"      font-weight="100"      font-size="16"      >          秋风的笔记      </text>    </svg>`;  return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;}const watermakr = document.createElement('div');watermakr.className = 'watermark';watermakr.style.backgroundImage = `url(${createWaterMark()})`document.body.appendChild(watermakr); 

明水印的破解一

以上就很快实现了水印的几种计划。然而对于有心之人来说,必定会想着破解,以上破解也很简略。

关上了 Chrome Devtools 找到对应的元素,间接按 delete 即可删除。

明水印的进攻

这样子的水印对于大略晓得控制台操作的小白就能够轻松破解,那么有什么方法能进攻住这样的操作呢?

答案是必定的,js 有一个办法叫做 MutationObserver,可能监控元素的改变。

MutationObserver 对古代浏览的兼容性还是不错的,MutationObserver是元素观察器,字面上就能够了解这是用来察看Node(节点)变动的。MutationObserver是在DOM4标准中定义的,它的前身是MutationEvent事件,最低反对版本为 ie9 ,目前曾经被弃用。

在这里咱们次要察看的有三点

  • 水印元素自身是否被移除
  • 水印元素属性是否被篡改(display: none ...)
  • 水印元素的子元素是否被移除和篡改 (element生成的形式 )

来通过 MDN 查看该办法的应用示例。

const targetNode = document.getElementById('some-id');// 观察器的配置(须要察看什么变动)const config = { attributes: true, childList: true, subtree: true };// 当察看到变动时执行的回调函数const callback = function(mutationsList, observer) {    // Use traditional 'for loops' for IE 11    for(let mutation of mutationsList) {        if (mutation.type === 'childList') {            console.log('A child node has been added or removed.');        }        else if (mutation.type === 'attributes') {            console.log('The ' + mutation.attributeName + ' attribute was modified.');        }    }};// 创立一个观察器实例并传入回调函数const observer = new MutationObserver(callback);// 以上述配置开始察看指标节点observer.observe(targetNode, config); 

MutationObserver次要是监听子元素的改变,因而咱们的监听对象为 document.body, 一旦监听到咱们的水印元素被删除,或者属性批改,咱们就从新生成一个。通过以上示例,加上咱们的思路,很快咱们就写一个监听删除元素的示例。(监听属性批改也是相似就不一一展现了)

// 观察器的配置(须要察看什么变动)const config = { attributes: true, childList: true, subtree: true };// 当察看到变动时执行的回调函数const callback = function (mutationsList, observer) {// Use traditional 'for loops' for IE 11  for (let mutation of mutationsList) {    mutation.removedNodes.forEach(function (item) {      if (item === watermakr) {          document.body.appendChild(watermakr);      }    });  }};// 监听元素const targetNode = document.body;// 创立一个观察器实例并传入回调函数const observer = new MutationObserver(callback);// 以上述配置开始察看指标节点observer.observe(targetNode, config); 

咱们关上控制台来测验一下。

这回完满了,可能完满抵挡一些开发小白了。

那么这样就十拿九稳了吗?显然,道高一尺魔高一丈,毕竟前端的一切都是不平安的。

明水印的破解二

在一个高级前端工程师背后,一切都是纸老虎。接下来我就轻易介绍三种破解的形式。

第一种

关上 Chrome Devtools,点击设置 - Debugger - Disabled JavaScript .

而后再关上页面,delete咱们的水印元素。

第二种

复制一个 body 元素,而后将原来 body 元素的删除。

第三种

关上一个代理工具,例如 charles,将生成水印相干的代码删除。

破解实际

接下来咱们实战一下,通过事后剖析,咱们看到某在线设计网站的内容是以 div 的形式实现的,所以能够利用这种计划。 关上 https://www.gaoding.com/design?id=33931419&simple=1 (仅供举例学习)

关上控制台,Ctrl + F 搜寻 watermark 相干字眼。(这一步是作为一个程序员的直觉,基本上你要找什么,搜寻对应的英文就能够 ~)

很快咱们就找到了水印图。发现间接删除,没有方法删除水印元素,依据咱们方才学习的,必定是利用了MutationObserver 办法。咱们应用咱们的第一个破解办法,将 JavaScript 禁用,再将元素删除。

水印曾经隐没了。

然而这样真的就高枕无忧了吗?

不晓得你有没有听过一种货色,看不见摸不着,然而它却实在存在,他的名字叫做暗水印,咱们将工夫倒流到 16 年间的月饼门事件,因为有员工将内网站点截图了,然而很快被定位出是谁截图了。

尽管你将一些可见的水印去除了,然而还会存在一些不可见的爱护版权的水印。(这就是避免一些好人拿去作另外的用处)

暗水印

暗水印是一种肉眼不可见的水印形式,能够放弃图片好看的同时,爱护你的资源版权。

暗水印的生成形式有很多,常见的为通过批改RGB 重量值的小量变动、DWT、DCT 和 FFT 等等办法。

通过介绍前端实现 RGB 重量值的小量变动 来揭秘其中的神秘,次要参考 不能说的机密——前端也能玩的图片隐写术 | AlloyTeam。

咱们都晓得图片都是有一个个像素点形成的,每个像素点都是由 RGB 三种元素形成。当咱们把其中的一个重量批改,人的肉眼是很难看出其中的变动,甚至是像素眼的设计师也很难分辨出。

你能看出其中的差异吗?依据这个原理,咱们就来实际吧。(女孩子能够把握办法后能够拿以下图片进行试验测试)

首先拿到以上图片,咱们先来解说解码形式,解码其实很简略,咱们须要创立一个法则,再通过咱们的法则去解码。当初假如的法则为,咱们将所有像素的 R 通道的值为奇数的时候咱们创立的通道明码,举个简略的例子。

例如咱们把以上当做是一个图形,退出他要和一个中文的 "一" 放进图像,例如咱们将 "一" 放入第二行。依照咱们的算法,咱们的图像会变成这个样子。

解码的时候,咱们拿到所有的奇数像素将它渲染进去,例如这里的 '5779' 是不是正好是一个 "一",上面就转化为实际。

解码过程

首先创立一个 canvas 标签。

 <canvas id="canvas" width="256" height="256"></canvas> 
var ctx = document.getElementById('canvas').getContext('2d');var img = new Image();var originalData;img.onload = function () {  // canvas像素信息  ctx.drawImage(img, 0, 0);  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);  console.log()  processData(ctx, originalData)};img.src = 'qiufeng-super.png'; 

咱们打印出这个数组,会有一个十分大的数组,一共有 256 256 4 = 262144 个值。因为每个像素除了 RGB 外还有一个 alpha 通道,也就是咱们罕用的透明度。

下面也说了,咱们的 R 通道为奇数的时候 ,就咱们的解密明码。因而咱们只须要所有的像素点的 R 通道为奇数的时候,将它填填充,不为奇数的时候就不填充,很快咱们就能失去咱们的暗藏图像。

var processData = function (ctx, originalData) {    var data = originalData.data;    for (var i = 0; i < data.length; i++) {        if (i % 4 == 0) {            // R重量            if (data[i] % 2 == 0) {                data[i] = 0;            } else {                data[i] = 255;            }        } else if (i % 4 == 3) {            // alpha通道不做解决            continue;        } else {            // 敞开其余重量,不敞开也不影响答案            data[i] = 0;        }    }    // 将后果绘制到画布    ctx.putImageData(originalData, 0, 0);}processData(ctx, originalData) 

解密完会呈现相似于以下这个样子。

那咱们如何加密的,那就相同的形式就能够啦。(这里都用了 不能说的机密——前端也能玩的图片隐写术中的例子,= = 我也能写出一个例子,然而感觉没必要,他人曾经写得很好了,咱们只是讲述这个办法,须要代码来举例而已)

编码过程

加密呢,首先咱们须要获取加密的图像信息。

var textData;var ctx = document.getElementById('canvas').getContext('2d');ctx.font = '30px Microsoft Yahei';ctx.fillText('秋风的笔记', 60, 130);textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data; 

而后提取加密信息在待加密的图片上进行解决。

var mergeData = function (ctx, newData, color, originalData) {    var oData = originalData.data;    var bit, offset;  // offset的作用是找到alpha通道值,这里须要大家本人动动脑筋    switch (color) {        case 'R':            bit = 0;            offset = 3;            break;        case 'G':            bit = 1;            offset = 2;            break;        case 'B':            bit = 2;            offset = 1;            break;    }    for (var i = 0; i < oData.length; i++) {        if (i % 4 == bit) {            // 只解决指标通道            if (newData[i + offset] === 0 && (oData[i] % 2 === 1)) {                // 没有信息的像素,该通道最低地位0,但不要越界                if (oData[i] === 255) {                    oData[i]--;                } else {                    oData[i]++;                }            } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)) {                // // 有信息的像素,该通道最低地位1,能够想想下面的斑点成果是怎么实现的                oData[i]++;            }        }    }    ctx.putImageData(originalData, 0, 0);} 

次要的思路还是我一开始所讲的,在有像素信息的点,将 R 偶数的通道+1。在没有像素点的中央将 R 通道转化成偶数,最初在 img.onload 调用 processData(ctx, originalData)

img.onload = function () {  // 获取指定区域的canvas像素信息  ctx.drawImage(img, 0, 0);  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);  console.log(originalData)    processData(ctx, originalData)}; 

以上办法就是一种比较简单的加密形式。以上代码都放到了仓库 watermark/demo/canvas-dark-watermark.html 门路下,办法都封装好了~。

然而理论过程须要更业余的加密形式,例如利用傅里叶变动公式,来进行频域制订数字盲水印,这里就不具体开展讲了,当前钻研完再具体讲~

破解实际

听完上述的介绍,那么某在线设计网站是不是很有可能应用了暗水印呢?

当然啦,通过我对某在线设计网站的剖析,我剖析了以下几种状况,咱们一一来进行测试。

咱们先通过收费下载的图片来进行剖析。关上 https://www.gaoding.com/design?id=13964513159025728&mode=user

通过试验(试验次要是去剖析他各个场景下触发的申请),发现在下载收费图片的时候,发现它都会去向阿里云发送一个 POST 申请,这相熟的申请域名以及相熟的数据封装形式,这不就是 阿里云 OSS 客户端上传形式嘛。这就好办了,咱们去查问一下阿里云是否有生成暗水印的相干形式,从而来看看某在线设计网站是否含有暗水印。很快咱们就从官网文档搜寻到了相干的文档,且对于低 QPS 是收费的。(这就是最好了解的连带效应,例如咱们感觉耐克阿迪啥卖静止类服饰,你买了他的鞋子,可能还会想买他的衣服)

const { RPCClient } = require("@alicloud/pop-core");var client = new RPCClient({  endpoint: "http://imm.cn-shenzhen.aliyuncs.com",  accessKeyId: 'xxx',  accessKeySecret: 'xxx',  apiVersion: "2017-09-06",});(async () => {  try {        var params = {          Project: "test-project",          ImageUri: "oss://watermark-shenzheng/source/20201009-182331-fd5a.png",            TargetUri: "oss://watermark-shenzheng/dist/20201009-182331-fd5a-out.jpg",            Model: "DWT"        };        var result = await client.request("DecodeBlindWatermark", params);                console.log(result);      } catch (err) {        console.log(err);      }})() 

咱们写了一个demo进行了测试。因为阿里云含有多种暗水印加密形式,为啥我应用了 DWT 呢?因为其余几种都须要原图,而咱们方才的测试,他上传只会上传一个文件到 OSS ,因而大抵上排除了须要原图的计划。

然而咱们的后果却没有发现任何加密的迹象。

为什么咱们会去猜测阿里云的图片暗水印的形式?因为从上传的角度来思考,咱们上传的图片 key 的地址即是咱们下载的图片,也就是当初存在两种状况,一就是通过阿里云的盲水印计划,另一种就是上传前进行了水印的植入。当初看来不是阿里云水印的计划,那么只是能是上传前就有了水印。

这个过程就有两种状况,一是生成的过程中退出的水印,前端退出的水印。二是物料图含有水印。

对于第一种状况,咱们能够通过 dom-to-image 这个库,在前端间接进行下载,或者应用截图的形式。目前通过间接下载和通过站点内生成,发现元素略有不同。

第一个为我通过 dom-to-image 的形式下载,第二种为站点内下载,显著大了一些。(有点狐疑他在图片生成中可能做了什么手脚)

然而感觉前端加密的形式比拟容易破解,最坏的状况想到了对素材进行了加密,然而这样的话就无从破解了(然而查阅了一些材料,因为某在线设计网站站点素材大多是通明背景的,这种加密成果可能会弱一些,当前牛逼了再来补充)。目前这一块临时还不分明,探索止步于此了。

攻打试验

那如果一张图通过暗水印加密,他的抵制攻击性又是如何呢?

这是一张通过阿里云 DWT暗水印进行的加密,解密后的样子为"秋风"字样,咱们别离来测试一下。

加一些元素

后果: 辨认成果不错

截图

后果: 辨认成果不错

大小变动

后果:辨认成果不错

加蒙层

后果: 间接就拉胯了。

可见,暗水印的抵制攻击性还是蛮强的,是一种比拟好的抵挡攻打的形式~

最初

以上仅仅为技术交换~ 大家不要在理论的场景自觉应用,商业我的项目违规应用后果自负 ~ 或者期待一下我接下来想搞的这个集体收费首图生成器~ 喜爱文章的小伙伴能够点个赞哦 ~ 欢送关注公众号 秋风的笔记 ,学习前端不迷路。

参考

https://imm.console.aliyun.com/cn-shenzhen/project?accounttraceid=1280c6af416744a38e9acf63c4e0878cjdet

https://help.aliyun.com/document_detail/138800.html?spm=a2c4g.11186623.6.656.3bd46bb4oglhEr

https://oss.console.aliyun.com/bucket/oss-cn-shenzhen/watermark-shenzheng/object?path=dist%2F

https://juejin.cn/post/6844903650054111246

https://www.zhihu.com/question/50677827/answer/122388524

https://www.zhihu.com/question/50735753

最初

如果我的文章有帮忙到你,心愿你也能帮忙我,欢送关注我的微信公众号 秋风的笔记,回复好友 二次,可加微信并且退出交换群,秋风的笔记 将始终陪伴你的左右。