前端水印
为什么要有水印的存在?
- 保护知识产权,防止未经允许被随意盗用。比如淘宝美团的图片,背后都有水印。
- 保护公司机密信息,防止有心之人泄密
通常来说,前后端都可以实现水印的添加。
- 前端水印适用场景:资源不跟某一个单独的用户绑定,而是一份资源,多个用户查看,需要在每一个用户查看的时候添加用户特有的水印,多用于某些机密文档或者展示机密信息的页面,水印的目的在于文档外流的时候可以追究到责任人
- 服务端水印使用场景:资源为某个用户独有,一份原始资源只需要做一次处理,将其存储之后就无需再次处理,水印的目的在于标示资源的归属人
从前端的角度来说,有哪些实现方案
DOM覆盖
利用div来做水印,需要两个关键css属性。use-select:none
和pointer-events
,不让用户选中我这个水印,以及让用户穿透我这个水印遮罩。
然后利用想要出现水印区域的宽高以及水印块的宽高,计算出我需要生成多少个水印块,然后铺开。
initDivWaterMark(userId: string) {
const waterHeight = 100
const waterWidth = 100
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')
wrap.setAttribute('class', 'watermark-item')
wrap.style.width = waterWidth + 'px'
wrap.style.height = waterHeight + 'px'
wrap.textContent = userId
this.box.appendChild(wrap)
}
}
ok,可以看到,我们的水印出现了。但是有一个问题是,使用dom重复生成的话,还不停的append
的话,感觉不优雅。而且一下就被人看到了,所以也可以用shadowdom
。
shadowdom ShadowDom MDN
Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。说白了就是隔离。
可以使用 Element.attachShadow()
方法来将一个 shadow root
附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
:
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot 属性:
let myShadowDom = myCustomElem.shadowRoot;
如果你将一个 Shadow root
附加到一个 Custom element
上,并且将 mode
设置为 closed
,那么就不可以从外部获取 Shadow DOM
了, myCustomElem.shadowRoot
将会返回 null。浏览器中的某些内置元素就是如此,例如,包含了不可访问的
Shadow DOM
。
所以为了简单,我们用了closed
initShadowdomWaterMark(userId: string) {
const shadowRoot = this.box.attachShadow({ mode: 'closed' })
const waterHeight = 100
const waterWidth = 100
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')
wrap.setAttribute('class', 'watermark-item')
// const styleStr = `
// color: #f20;
// text-align: center;
// transform: rotate(-30deg);
// `
// wrap.setAttribute('style', styleStr)
// wrap.setAttribute('part', 'watermark')
wrap.style.width = waterWidth + 'px'
wrap.style.height = waterHeight + 'px'
wrap.textContent = userId
shadowRoot.appendChild(wrap)
}
}
可有看到,这样写,样式没有了,其实就是因为 shadowdom
起到了隔离的作用,微前端里很重要的一个点就是沙箱隔离,其中像qiankun
这样的框架的css
隔离就是用了shadowdom
使用内联或者使用特殊的:伪类也可以解决,这里就先直接内联了。 Css part
canvas/svg背景图
可以看到,不管是使用dom还是shadowdom,都避免不了的进行for循环来进行添加元素,还需要计算。依然不是那么的优雅。所以我们可以考虑用canvas输出一个背景图,然后通过background-repeat: repeat
来实现。
getCanvasUrl(userId: string) {
const angle = -30
const txt = userId
this.canvas = document.createElement('canvas')
this.canvas.width = 100
this.canvas.height = 100
this.ctx = this.canvas.getContext('2d')!
this.ctx.clearRect(0, 0, 100, 100)
this.ctx.fillStyle = '#f20'
this.ctx.font = `14px`
this.ctx.rotate((Math.PI / 180) * angle)
this.ctx.fillText(txt, 0, 50)
return this.canvas.toDataURL()
}
可以看到,我们只用了一个标签,以及背景图的方式,来实现了一个水印,但是呢,如果你是一个有心之人,我们只需要动动手指,打开F12,把这个标签删了,或者修改它的背景,都可以把水印去掉。那我们应该怎么办呢?那就需要用到MutationObserver
了,说到观察者,现在使用的频率也是越来越高了。
MutationObserverMDN
MutationObserver应用的方式还挺多的
- 如我们上周提到的
guide mask
的解决方案问题。我们可以通过mutationObserver
对它要笼罩的父级节点进行监控,并设置一个超时时间disconnect
,在时间内对它进行矫正。我曾经有做过一个锚点的功能,也利用到了它来进行矫正操作。 - 我们可以通过
MutationObserver
来对真正的可用性能进行监控,通过判断节点的增加趋势,来获得真正可以使用的时间点。 Vue nexttick
的实现原理,利用MutationObserver
是个micro task
,来进行下一tick的通知。当然是promise.then
不好使的情况下,模拟实现如下function myNextTick(func) { var textNode = document.createTextNode('0'); //新建文本节点 var callback = (mutationsList, observer) => { func.call(this); }; var observer = new MutationObserver(callback); observer.observe(textNode, { characterData: true }); textNode.data = '1'; //修改文本信息,触发dom更新 }
- 监控我们的水印节点是否被变更,我们也可以针对性的进行恢复。
initObserver() {
// 观察器的配置
const config = { attributes: true, childList: true, subtree: true }
// 当观察到变动时执行的回调函数
const callback: MutationCallback = (mutationsList, observer) => {
for (const mutation of mutationsList) {
mutation.removedNodes.forEach((item) => {
if (item === this.box) {
this.warnningTargetChanged = true
// 省事,直接添加在body上了
document.body.appendChild(this.box)
}
})
}
if (this.warnningTargetChanged) {
this.warnningTargetChanged = false
console.log(`用户${this.userId}的水印变动了,可能涉嫌违规操作!!!`)
}
}
// 监听元素
const targetNode = document.body
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback)
// 以上述配置开始观察目标节点
observer.observe(targetNode, config)
}
当然,并不是说这样就万无一失了,因为我们还可以通过 disabled javascript来解决。像有的网站,开启F12就会无限循环debugger,也可以解决。
暗水印
那如果说,就是有这样的人存在,统统都能搞定呢?这时候就需要隐藏水印的出现了。比如大众点评上面的图片,其实也都是有隐藏版权的水印存在的。如果是商用盗用,都是能被查到的。
暗水印的生成方式有很多,常见的为通过修改RGB 分量值的小量变动
、DWT、DCT 和 FFT 等方法。DFT、DCT和DWT的联系和区别。
前端实现主要看RGB 分量值的小量变动。
我们都知道图片都是有一个个像素点构成的,每个像素点都是由 RGB 三种元素构成。当我们把其中的一个分量修改,人的肉眼是很难看出其中的变化,甚至是像素眼的设计师也很难分辨出。
那么,我们只要能获取到一张图片上每个像素点上的具体信息,就可以再RGB上动动手脚,就可以把我们想要的信息藏进去。那如何获取像素点的信息呢?
就需要用到 canvas 的 CanvasRenderingContext2D.getImageData()
了,这个方法会返回一个 ImageData
对象,其中就包含了像素的信息数组。所以我们应该可以利用这个方法,来做取色器
这个一维数组存储了所有的像素信息,一共有 256 256 4 = 262144 个值。其中 4 个值一组,为什么呢?在浏览器中解析图片,除了 RGB 值外,每组第 4 个值为透明度值,即像素信息实际为大家熟知的 rgba 值。
以我们想要藏的文字信息为例
const txt = '测试点'
this.canvas = document.createElement('canvas')
this.canvas.width = 10
this.canvas.height = 10
this.ctx = this.canvas.getContext('2d')
this.ctx.clearRect(0, 0, 10, 10)
this.ctx.font = `14px`
this.ctx.fillText(txt, 0, 0)
const textData = this.ctx.getImageData(0, 0, 10, 10).data
把上面代码复制到控制台可以发现,字体的数据基本都是0,0,0,xx。
那现在我们有了文字数据和图片数据,我们就可以设计一个算法。以R通道为例子,这个是红色通道,(255, 0, 0, 255)
就代表的是纯红色。
我们遍历图片数据,检查它的每一个R点位,如果这个点位的文字数据是有的,也就是alpha
值不为0,那我们就强行把当前图片信息的这个点的值改成奇数,如果这个点位没有数字,就把它改成偶数。
那么最后,这个图片数据里奇数的部分,就是有文字盖着的部分。而偶数部分,就是无关紧要的了。那最后想要找到我们的目标文案的时候,只需要把奇数部分的值变成255,把其他通道以及偶数部分的,全都改成0。 文字就出现了。
// 加密核心方法
for (let i = 0; i < oData.length; i++) {
if (i % 4 === bit) {
// 如果文字在这里没有数据,并且图片R通道是奇数,那我们把它改成偶数。
if (newData[i + offset] === 0 && oData[i] % 2 === 1) {
// 没有信息的像素,该通道最低位置0,但不要越界
if (oData[i] === 255) {
oData[i]--
} else {
oData[i]++
}
// 如果文字在这里是有数据的,并且图片R通道是偶数,那我们把它改成奇数。
} else if (newData[i + offset] !== 0 && oData[i] % 2 === 0) {
oData[i]++
}
// 也就是说,如果是奇数,说明一定是有文字压在上面的
}
}
// 解密核心方法
for (let i = 0; i < data.length; i++) {
// R通道
if (i % 4 === 0) {
// 目标分量,把偶数的关闭。因为文字没有数据在这里。
if (data[i] % 2 === 0) {
data[i] = 0
} else {
data[i] = 255
data[i + 3] = 255
}
} else if (i % 4 === 3) {
continue
} else {
data[i] = 0
}
}
但是我们实际想要见到的隐藏水印的形式,肯定不是局限在图片上的。我们希望的是给我们整体的文章之类的打上隐藏水印。那这怎么做呢?
其实我们可以把之前cavans
的水印,把颜色换成黑色,旋转去掉,透明度降低到0.005
,为啥是这个值,因为0.005 * 255=1.27
。基本算是最小透明度了。但是因为我们是高层级,所以截图的时候,一定会把这信息包含进去。那么我是(0,0,0,1)
叠加到原来的图片上,至少会影响到原图的颜色,也就是说它的R通道,起码会动个1。我编不下去了。我也不知道为啥。但是确实可以。大家可以一起探讨下。