不过放在前端的水印都是比较容易被去除的,只要在浏览器 Devtools 中找到水印所在 DOM 元素删除即可。于是聪明的前端开发们使用 MutationObserver 造出了不死水印:只要水印被删除或者样式被修改,就会重新创建一个水印。
以某个国产水印库为例,不死水印的原理如下(有简化)
//监听dom是否被移除或者改变属性的回调函数
var domChangeCallback = function (records) {
// ...
if ((globalSetting && records.length === 1) || (records.length === 1 && records[0].removedNodes.length >= 1)) {
// 如果被删除,则重新创建水印图层
loadMark(globalSetting)
}
}
var hasObserver = MutationObserver !== undefined
var watermarkDom = hasObserver ? new MutationObserver(domChangeCallback) : null
var option = {
childList: true, // 防止被删除,水印外层需要额外包一层 div
attributes: true, // 防止 style 被修改
subtree: true, // 防止被删除
}
// ...
watermarkDom.observe(document.getElementById(defaultSettings.watermark_id).shadowRoot, option)
//监听dom是否被移除或者改变属性的回调函数
var domChangeCallback = function (records) {
// ...
if ((globalSetting && records.length === 1) || (records.length === 1 && records[0].removedNodes.length >= 1)) {
// 如果被删除,则重新创建水印图层
loadMark(globalSetting)
}
}
var hasObserver = MutationObserver !== undefined
var watermarkDom = hasObserver ? new MutationObserver(domChangeCallback) : null
var option = {
childList: true, // 防止被删除,水印外层需要额外包一层 div
attributes: true, // 防止 style 被修改
subtree: true, // 防止被删除
}
// ...
watermarkDom.observe(document.getElementById(defaultSettings.watermark_id).shadowRoot, option)
当水印被删除 (childList
) 或者 style 被修改 (attributes
) 时,MutationObserver
就会触发回调,重新绘制水印,让我们无法通过正常方式删除。当然还有一些变种的不死水印,在被删除 / 修改时会清空页面或者后台上报删除事件,让人防不胜防。对付这些水印,我们可以在控制台上禁用 JavaScript 来防止这些骚操作,然后尽情地去除水印再进行截图。
不过这样还是有点麻烦,如果想要一个没有水印的干净清爽的世界,有更好的方法吗?
既然不死水印使用的是 MutationObserver
,我们可以通过油猴脚本,我们可以在页面脚本之前把这个东西干掉。
// ==UserScript==
// @run-at document-start
// ==/UserScript==
delete window.MutationObserver
delete window.WebKitMutationObserver
delete window.MozMutationObserver
// ==UserScript==
// @run-at document-start
// ==/UserScript==
delete window.MutationObserver
delete window.WebKitMutationObserver
delete window.MozMutationObserver
大公告成!不过有些站点本身就会使用 MutationObserver
,贸然删除会导致功能不可用,如果要精准控制的话就需要自己写 Proxy
劫持 construct
来处理了,这样又会麻烦起来。那么有没有一劳永逸的方法呢?
其实还是有的,也就是本文的正题了——
水印制作者需要花费很大力气保证水印可见并且可以恢复出信息,而去除水印只需要让它不可见就可以了。那么有没有办法在不修改水印本身样式的情况下让它不可见呢?答案就是使用 CSS 从“旁路”攻击水印。
假设我们有一个水印图层长这样
<div id="wm_div_id" style="pointer-events: none !important; display: block !important"></div>
<div id="wm_div_id" style="pointer-events: none !important; display: block !important"></div>
虽然水印已经内联了 important
样式防止被 display: none
掉,但我们还有非常多的方式可以让它不可见:
#wm_div_id {
/* 通过使其透明让它不可见 */
opacity: 0;
/* 修改可见性为 hidden 让它不可见 */
visibility: hidden;
/* 让它尺寸为 0 不可见 */
width: 0;
height: 0;
/* 设置它的位置在屏幕之外让它不可见 */
position: absolute;
top: -9999;
/* 让它被背景遮住不可见 */
z-index: -100;
}
#wm_div_id {
/* 通过使其透明让它不可见 */
opacity: 0;
/* 修改可见性为 hidden 让它不可见 */
visibility: hidden;
/* 让它尺寸为 0 不可见 */
width: 0;
height: 0;
/* 设置它的位置在屏幕之外让它不可见 */
position: absolute;
top: -9999;
/* 让它被背景遮住不可见 */
z-index: -100;
}
让水印不可见并不是我们对水印元素做了任何修改,我们只是让 CSS selector 命中了这个元素并且让它看不见而已。不管是透明还是移动到屏幕之外,只要在可视区域内看不到这个元素,我们的目标就达成了。计算样式变化是不会导致 MutationObserver
触发任何回调的,而目前的 Web 规范中也暂时没有能监听计算样式变化的 API,好耶!
出于某些原因我不可以传授去水印的代码,但自己编写一个并不是什么困难的事——只要让 CSS 选择器能够选择到目标元素即可。
此外前端生成的水印图层常常会有很明显的特征可以被 CSS selector 选中,发动一点小脑筋1很快就可以写出一个能处理掉大部分站点水印的样式规则,再使用 stylish 之类的浏览器插件加载到每个网页上,这下我们可以在完全不被水印影响的情况下尽情冲浪了。
尽管 CSS 样式攻击很难防御,JS 也不是没有应对方法:Element.getComputedStyle()
可以获取特定元素的计算样式,JS 获取到计算样式之后可以检查是否被修改再重新绘制水印。获取计算样式由于会触发 RecalculateStyle 和 Layout 所以对性能可能会有影响(不过可以在 rAF 里面处理)。这样做的成本真的很高,应该不会真的有人这么做吧.jpg
相信大家已经完全掌握了这门技术,学会编写自己的攻击样式了吧。我可没有传授什么去内网水印的教程(
冲浪愉快2 :)
水印图层通常会有类似 wm
watermark
字样的属性,合理运用 attribute selector 可以事半功倍 ↩
据我所知,目前也有一些公司在实验其他更隐蔽更难以去除的水印,例如基于字形/字符间距变化的水印,谨慎冲浪 :( ↩