共计 10704 个字符,预计需要花费 27 分钟才能阅读完成。
从破解某定设计网站谈前端水印(具体教程)
前言
最近在写公众号的时候,经常会本人做首图,并且缓缓地发现沉迷于制作首图,感觉扁平化的设计的真好好看。缓缓地萌发了一个做一个属于本人的首图生成器的想法。
制作呢,当然也不是拍拍脑袋就开始,在开始之前,就去钻研了一下 某在线设计网站(如果有人不晓得的话,能够说一下,这是一个在线制作海报之类的网站 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
最初
如果我的文章有帮忙到你,心愿你也能帮忙我,欢送关注我的微信公众号 秋风的笔记
,回复 好友
二次,可加微信并且退出交换群, 秋风的笔记
将始终陪伴你的左右。