水印的攻防战

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

水印的创建

先假设有如下浅蓝色区域的内容需要添加上以掘金 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> 节点或修改属性的方式去除水印,接着奏乐,接着舞。

相关推荐
CoolerWu16 分钟前
TRAE SOLO实战成功展示&总结:一个所见即所得的笔记软体
前端·javascript
Cassie燁23 分钟前
el-button源码解读1——为什么组件最外层套的是Vue内置组件Component
前端·vue.js
vx_bscxy32223 分钟前
告别毕设焦虑!Python 爬虫 + Java 系统 + 数据大屏,含详细开发文档 基于web的图书管理系统74010 (上万套实战教程,赠送源码)
java·前端·课程设计
北极糊的狐24 分钟前
Vue3 子组件修改父组件传递的对象并同步的方法汇总
前端·javascript·vue.js
spionbo24 分钟前
Vue3 前端分页功能实现的技术方案及应用实例解析
前端
Zyx200725 分钟前
JavaScript 作用域与闭包(下):闭包如何让变量“长生不老”
javascript
AI绘画小3326 分钟前
Web 安全核心真相:别太相信任何人!40 个漏洞挖掘实战清单,直接套用!
前端·数据库·测试工具·安全·web安全·网络安全·黑客
7***n7528 分钟前
前端设计模式详解
前端·设计模式·状态模式
u***j32430 分钟前
JavaScript在Node.js中的进程管理
开发语言·javascript·node.js
用户479492835691535 分钟前
Vite 中 SVG 404 的幕后黑手:你真的懂静态资源处理吗?
前端·vite