水印的攻防战

有时候我们的网站有些内容需要添加上水印,有时候我们浏览下载一些有水印的资料时又想去除水印。本篇文章就来说说关于水印的创建与去除,以及防止删除水印的手段,最后是反制水印防删的方法。

水印的创建

先假设有如下浅蓝色区域的内容需要添加上以掘金 logo 作为单个图像的水印:

该区域使用了一个 div 生成:

html 复制代码
<div class="content">比如说我是一块需要打上水印的区域</div>

样式如下:

css 复制代码
.content {
  width: 600px;
  height: 400px;
  background-color: aliceblue;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
}

我将使用 canvas(关于 canvas 的更多知识,可以移步《canvas 实现卫星绕月动画》) 来创建水印,canvas 的创建在引入的 index.js 中定义,而不是直接在 html 文件使用 <canvas>,目的在于方便后续的防删除处理 :

javascript 复制代码
// index.js
class DrawWaterMark {
 constructor(width, height) {
    // 水印区域的宽高
    this.width = width
    this.height = height
    this.init()
  }

  init() {
    const canvas = document.createElement('canvas')
    // 如果不支持 canvas
    if (!canvas.getContext) {
      return
    }
    canvas.width = this.width
    canvas.height = this.height
    // 通过 style 添加样式,避免 css 被直接修改
    canvas.style.position = 'absolute'
    canvas.style.top = 0
    canvas.style.left = 0
    canvas.style.opacity = 0.2

    bodyNode.appendChild(canvas)
    const ctx = canvas.getContext('2d')
    this.draw(ctx)
  }

  draw(ctx) {
    // 创建临时 canvas,目的在于控制图片大小
    const width = 200
    const height = 100
    const tempCanvas = document.createElement('canvas')
    tempCanvas.width = width
    tempCanvas.height = height

    const tempCtx = tempCanvas.getContext('2d')
    tempCtx.save()
    tempCtx.translate(width / 2, height / 2)
    tempCtx.rotate((Math.PI / 180) * 30)

    const juejin = new Image()
    juejin.src = './imgs/logo.svg'

    juejin.onload = () => {
      tempCtx.drawImage(juejin, 0, 0, 48, 10)
      tempCtx.restore()
      const pattern = ctx.createPattern(tempCanvas, 'repeat')
      ctx.fillStyle = pattern
      ctx.fillRect(0, 0, this.width, this.height)
    }
  }
}

const drawWaterMark = new DrawWaterMark(600, 400)

其中,ctx.createPattern() 是使用指定的图像创建模式的方法:

  • 其第 1 个参数为作为重复图像源的对象,它的值可以是个图片元素或 canvas 元素等,也就是说我们可以直接通过 const img = new Image(),让 img.src 的值为 png 或 svg 等图片格式的掘金 logo,然后以 img 作为第 1 个参数的值。但是为了控制水印中单个图像的尺寸大小,我创建了一个临时的 canvas 元素 tempCanvas,这样就可以控制 tempCanvas 的宽高,并移动旋转其默认坐标空间,让绘制出来的掘金 logo 呈现旋转 30° 的效果。
  • 第 2 个参数就是指定如何重复图像,传入 'repeat' 意为在水平和垂直方向均重复图像。

返回值 pattern 可以作为 ctx.fillStyle 的值,最后通过 ctx.fillRect() 绘制出在 x、y 轴方向重复掘金 logo 的水印效果:

水印的删除

想要删除掉如上创建的水印,简直易如反掌,只需小手一点,按下 f12 调出调试工具,然后找到 canvas 元素,鼠标右击,选择"删除元素"即告成功:

或者也可以选择修改 canvas 的 style 样式,调整透明度或者设置上 display: none 等,也可以起到让水印消失的目的:

水印的防删(MutationObserver)

为了防止有人使用以上所示的小诡计删除我们辛苦创建的水印,可以借助 MutationObserver,它提供了监视对 DOM 树所做更改的能力,之前讲事件循环的时候在微任务队列提到过它。通过 new MutationObserver() 创建一个观察器实例 observer,传入的 callback 是一个回调函数,会在指定的 DOM 发生变化时调用:

javascript 复制代码
const callback = mutationsList => {
  console.log(mutationsList)
}
const observer = new MutationObserver(callback)

比如我们想监听 <canvas> 节点,先去获取它:

javascript 复制代码
const canvasNode = document.querySelector('canvas')

然后就可以通过之前创建的实例对象 observerobserve() 方法,传入的第 1 个参数为要观察的 DOM 节点, 第 2 个参数是一个配置对象 options

javascript 复制代码
observer.observe(canvasNode, {
  attributes: true
})

attributestrue 表示观察所有监听的节点属性值的变化,当我们去改变 <canvas> 的属性,比如修改了 style 属性,让 opacity 为 0,就会触发 callback,打印 mutationsList

mutationsList 是一个数组(可以同时监听多个节点),里面记录着被修改节点的信息:

如此一来,当有人想通过修改样式的方法去除水印时,就会被我们当场逮捕。但是请注意,当被观察的 <canvas> 本身被删除时,是不会触发 callback 的,所以我们应该监听 <canvas> 节点的父节点 <body>

javascript 复制代码
const bodyNode = document.querySelector('body')
observer.observe(bodyNode, {
  childList: true,
  subtree: true,
  attributes: true
})

childListtrue,表示监听 <body> 节点中发生的节点的新增与删除,这样我们就能监听到是否有人偷偷地删除了 <canvas>

展开 [MutationRecord] 如下:

subtreetrue,以表示监听以 <body> 为根节点的整个子树,配合 attributes: true 就能同时监听到作为子节点的 <canvas> 的属性是否改变。

一旦监听到有人对 <canvas> 做了手脚,我们就可以在传入 new MutationObserver(callback)callback 回调中进行处理:

javascript 复制代码
const callback = mutationsList => {
  mutationsList.forEach(recode => {
    // 如果是删除 canvas
    if (recode.target.localName === 'body') {
      recode.removedNodes.forEach(node => {
        if (node.localName === 'canvas') {
          this.init()
        }
      })
    }
    // 如果是修改 canvas 属性
    if (recode.target.localName === 'canvas') {
      bodyNode.removeChild(recode.target)
    }
  })
}

处理时分 2 种情况:

  1. 如果是直接删除了 <canvas>,那么 callback 调用时传入的 mutationsList 的数组中,就会有一条记录的 target 指向 body,展开 target 对象可以看到其有个 localName 属性,值为 "body"

并且 removedNodesNodeList [canvas],既然类型为 NodeList,那么对于 removedNodes 就不能随便使用数组方法了,但是依然可以使用 forEach 进行遍历,判断是否存在 localName"canvas" 的成员。如果有,证明删除了 <canvas>,则再次调用我们定义的 init() 实例方法,再次生成水印。

  1. 如果是改变了 <canvas> 的属性,那么 mutationsList 数组中就会有一条记录的 target 指向 canvas。此时我们可以直接删除掉原本的 canvas,就会再次触发 callback,按情况 1 处理。

现在,当有人想去除水印时,就会如下图所示,无功而返:

反制防删

尽管我们做了一些措施防止水印被人去除,但这些措施都是基于 js 的,只需要点击如下图所示的"设置":

勾选上"禁用 JavaScript":

那么依旧可以轻松通过删除 <canvas> 节点或修改属性的方式去除水印,接着奏乐,接着舞。

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_7482402510 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar10 小时前
纯前端实现更新检测
开发语言·前端·javascript