前端水印破解成本的循序渐进
起因
文心一言 的体验账号开通了,拿到账号后试了下AI图片生成觉得挺有意思的, 想分享,但碍于水印(虽然用户规则没提及不能分享)的存在,然后就学习和研究了下页面里的水印技术(如何破解)。
顺便总结了下从 增加用户破解成本 的角度来看水印的实现方案。为什么要从 从用户破解成本 的角度看水印的技术演进,个人觉得技术的攻防没有尽头,在合适的成本下我们只需要让绝大部分用户 知难而退 就够了。
文心页面水印的实现方案
MutationObserver(监听DOM元素的变动) + shadowRoot(隐藏DOM,但是里面使用的是文本渲染的方式,用base64图片会更好) + Disable debugger(写完文章后想录制GIF才发现页面新增了这个策略)
无法提供对应(删除水印后会重新加回节点)的GIF了,o(╥﹏╥)o,写这篇文章的时候页面还没升级控制台 禁止调试 策略,想着后再录,结果。。。
水印实现及破解成本的循序渐进
★ background-repeat
场景: 主动对用户进行弱提示
<!DOCTYPE html>
<head>
<title>background-repeat</title>
<style>
html {
background-image: url('水印.svg');
background-repeat: repeat;
}
</style>
</head>
<body>this is Content</body>
</html>
通过上面的代码,我们简单的在页面背景引入水印图片实现了一个带有水印的页面。
★ ★ shadowDom
这个是在文心页面中发现的,个人认为实际的作用应该是为了隔离,同时起到混淆隐藏水印节点作用, 不过隐藏节点的实现最好还是用非文本节点。
首先,我们先看上面 background-repeat 样例实现的效果,由下图中可以看到这个节点几乎是显式的(用户只需简单的对dom节点选中即可发现)。
再来看用 shadowDom 的实现:
<!DOCTYPE html>
<head>
<title>shadow-dom</title>
<style>
html, body {
width: 100%;
height: 100%;
}
#shadow-root {
pointer-events: none;
}
</style>
<script>
window.onload = function () {
const rootEle = document.querySelector('#shadow-root')
const shadowRoot = rootEle.attachShadow({ mode: 'closed' })
shadowRoot.innerHTML = `
<div style="position: absolute;width: 100%;height: 100%;z-index: 99999;background-image: url('data:image/svg+xml,xxxxxx');"></div>`
}
</script>
</head>
<body>
<div>这是正文,这是正文,这是正文。</div>
<div id="shadow-root"></div>
</body>
</html>
实现效果:
上图的 case 由于节点的数量比较少,看起来还是比较容易发现水印的节点。
但在实际的页面中,节点的数量是非常多的,比如下面的文心页面就比较不易发现(比较耗时):
★ ★ ★ MutationObserver
作用:通过监听水印DOM节点的变动,进一步阻止用户去掉水印的行为
先看看代码的实现:
<!DOCTYPE html>
<head>
<title>mutation-observer</title>
<style>
html, body {
width: 100%;
height: 100%;
}
#shadow-root {
pointer-events: none;
}
</style>
<script>
window.onload = function () {
// 生成节点
function createWaterMarkDom () {
let rootEle = document.querySelector('#shadow-root')
console.log('rootEle', rootEle)
if (!rootEle) {
rootEle = document.createElement('div')
rootEle.setAttribute('id', '#shadow-root')
document.body.appendChild(rootEle)
}
const shadow = rootEle.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<div id="shadow" style="color: #999;font-size: 35px;position: absolute;width: 100%;height: 100%;z-index: 99999;background-image: url('data:image/svg+xml, xxxx');">水印...</div>`
}
createWaterMarkDom()
// 监听body下的元素变动
const bodyNode = document.body
const observerCallback = function(mutations, observer) {
// @TODO 手动delete掉,再加回来
console.log('mutations', mutations)
console.log('observer', observer)
}
const observerConfig = {
attributes: true, // 节点的特性
childList: true, // 节点的子节点的更改
subtree: true, // 节点所有后代的更改
characterData: true // 节点的文本内容
}
const bodyObserver = new MutationObserver(observerCallback)
bodyObserver.observe(bodyNode, observerConfig)
// 监听shadowRoot宿主元素的变动
const shadowRootNode = document.querySelector('#shadow-root')
const shadowRootObserver = new MutationObserver(observerCallback)
shadowRootObserver.observe(shadowRootNode, observerConfig)
// 监听shadow元素的变动
const shadowNode = shadowRootNode.shadowRoot.querySelector('#shadow')
const shadowObserver = new MutationObserver(observerCallback)
shadowObserver.observe(shadowNode,observerConfig)
}
</script>
</head>
<body>
<div>节点</div>
<div id="shadow-root"></div>
<div>其他节点</div>
<div>这是正文,这是正文,这是正文。</div>
</body>
</html>
看下实现效果:
可以看到只要对水印节点操作,就会触发 callback, 然后在回调函数里先删除水印节点,再添加回来即可。
虽然这种方式已经尽可能的增加用户去除水印的操作成本,但是通过浏览器的控制台设置,还是可以让这种方案沦陷。
但是这种方式在页面加载完成后,选择开启 Disable JavaScript 后,我们的 Observer 就失效了。
★ ★ ★ ★ Disable debugger
作用: 限制用户在控制台调试,阻止用户对 Disable JavaScript 的设置
(function (){
const temp = Object.create(null)
let t = Date.now()
Object.defineProperty(temp, 'v', {
get: function() {
if(Date.now() - t > 100){
alert('非法操作,将记录系统')
window.location.href = "about:blank"
}
}
})
setInterval(function(){
t = Date.now()
debugger
console.log(temp.v)
}, 200)
})()
实现效果如下:点击确定将会跳转空白页,实际页面中我们打包代码会通过压缩混淆的方式,使得 source 里的代码没那么直观。
当然还有更高级别的反调试(比如:爆栈等),反调试的攻防一山还有一山高,就不进行讨论了。在这里我们只针对 增加用户对水印的破解成本 探讨而简单举的例子。
★ ★ ★ ★ ★ 隐式水印
作用:不可见,肉眼无法识别,多用于版权保护盗用追溯等,破解成本对于普通用户较高
这里就不去实现,具体的实现可看下 不能说的秘密——前端也能玩的图片隐写术 的实现,也有第三方的服务商的比如:阿里云防泄漏数字水印服务、腾讯云盲水印服务;