XingYun blog
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具

XingYun

冲!
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具
  • 算法图解阅读笔记
  • MD5算法
  • JS实现常见排序
  • 邓俊辉算法与数据结构
  • JS实现队列
  • JS实现树的几种遍历方式
  • 二叉树相关
  • 哈希算法
  • 进制转换与js实现
  • JS实现链表
  • JS实现栈与栈应用
  • 算法与数据结构
XingYun
2023-07-20
目录

图片滑动验证码

# 图片滑动验证码

从用户体验来看,用户只需要使用鼠标拖动滑块到指定位置,如果位置正确,则通过验证。这是一种十分好用的验证方式,相比输入形式的验证码,省去了用户键盘的输入过程。

这类验证码的实现过程一般如下:

  1. 服务端准备图片若干个。

  2. 用户请求获取图片验证码,服务端随机选取一个图片作为验证码大图,复制指定形状的一小部分图片内容作为滑块。

  3. 记录复制的滑块位置,响应大图,滑块图给前端。

  4. 用户拖动滑块拼图,把滑动的位置信息发送给服务器。

  5. 服务器验证滑动位置是否和记录的相近,允许一定像素的误差,验证是否通过。

# 一个例子

以 https://beian.miit.gov.cn 网站为例

跳过前面的步骤, 我们请求验证返回数据剔除无用信息:

{
  "code": 200,
  "msg": "操作成功",
  "params": {
    "smallImage": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA0JCgsKCxxxxxxxxxxxxxx",
    "bigImage": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA0JCgsKCA0lFAj/2Qxxxxxxxxxxx",
    "uuid": "87db2dbb-4c1d-4633-a624-0045305e61e3",
    "height": "81"
  },
  "success": true
}
1
2
3
4
5
6
7
8
9
10
11

bigImage smallImage 是 base64 编码后的图片,一个是大图,一个是用于滑动的方块,其中的 uuid 可以认为是图片 id,在图片验证时需要传给服务器。

如果我们拿 bigImage 内容进行 Base64 转图片,可以看到验证码大图。

验证码校验

我们滑动图片验证码到准确位置,这时会发起验证请求,我拷贝出了这条请求的详细信息。

curl 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/image/checkImage' \
  --data-raw '{"key":"6af2c1bb-47a4-479d-a9f1-1ab17f86214e","value":"320"}' \
  --compressed
1
2
3

从中可以发现,依旧在请求头中携带了 Token 字段。同时请求体携带了 key 和 value 两个字段,其中 key 就是上一步获取到的图片的 uuid,value 则是滑动验证码滑动的距离。320 也就是表示滑动了 320 个像素位置进行了拼图。

验证通过后得到响应信息,其中 params 是验证后得到的可以用于请求验证的值。

{
  "code": 200,
  "msg": "操作成功",
  "params": "eyJ0eXBlIjozLCJleHREYXRhIjp7InZhZnljb2RlX2ltYWdlX2tleSI6IjZhZjJjMWJiLTQ3YTQtNDc5ZC1hOWYxLTFhYjE3Zjg2MjE0ZSJ9LCJlIjoxNjg5MDg4NDQ0MTM2fQ.cYPpQya1KiWZgC3T8ZNr9cWAM2aTHDtFUpzPIO-sMhQ",
  "success": true
}
1
2
3
4
5
6

到这里 滑动验证码 验证过程完成

验证码校验这个阶段,因为要计算验证码的滑动距离,这里需要涉及到图像处理知识,该怎么破解它?

# 验证码图片分析

我们获取几张验证图片观察,明显看到在要滑动的目标区域有被阴影覆盖,那么这一部分的 RGB 颜色就有所变化,一个正常的图片很少有这种矩形方块的颜色突变。

通过上面的分析,我们发现色彩其实并不重要,图片的灰度变化(或者叫亮度变化)比较重要,因为目标区域有被灰色覆盖。会产生一个矩形突变,那么我们可以把图片转换成灰度图片,这样更有利于观察。

准备验证图片一张

我们把这个图片进行解码,然后转换成灰度图片。

其中灰度值通过公式

R * 0.2126 + G * 0.7152 + B * 0.0722
1

计算得到。

以下为 JS 转换代码

function main(base64String) {
  const decodedBytes = atob(base64String)
  const byteNumbers = new Array(decodedBytes.length)
  for (let i = 0; i < decodedBytes.length; i++) {
    byteNumbers[i] = decodedBytes.charCodeAt(i)
  }
  const byteArray = new Uint8Array(byteNumbers)

  const blob = new Blob([byteArray], { type: 'image/png' })
  const imageURL = URL.createObjectURL(blob)

  const image = new Image()
  image.onload = function () {
    const canvas = document.createElement('canvas')
    canvas.width = image.width
    canvas.height = image.height
    const context = canvas.getContext('2d')
    context.drawImage(image, 0, 0)
    const imageData = context.getImageData(0, 0, canvas.width, canvas.height)

    // 将图片像素点转换为黑白
    for (let i = 0; i < imageData.data.length; i += 4) {
      const red = imageData.data[i]
      const green = imageData.data[i + 1]
      const blue = imageData.data[i + 2]
      const gray = Math.round(red * 0.2126 + green * 0.7152 + blue * 0.0722)

      // 将像素点的颜色设置为非黑即白
      imageData.data[i] = gray
      imageData.data[i + 1] = gray
      imageData.data[i + 2] = gray
    }

    context.putImageData(imageData, 0, 0)

    // 保存黑白图片
    const link = document.createElement('a')
    link.href = canvas.toDataURL()
    link.download = 'gray.jpg'
    link.click()
    URL.revokeObjectURL(imageURL)
    console.log('验证码图片已保存')
  }
  image.src = imageURL
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

将图片转换成

其实这时,如果我们输出图片的每个像素点的灰度矩阵,已经可以发现灰色方块处的灰度特点了。可以很明显的看到灰色方块区域的灰度值变化。

例:

如果你检测这个灰度矩阵,你已经有办法找到灰色区域开始的位置了。

# 图片二值化

为了更方便的分析,我们可以对图片再次转化,我们可以选取一个阈值,超过指定阈值的像素点直接转换成白色,低于指定阈值的颜色转换成黑色。这样更方便后续的分析。

改造代码,添加亮度阈值判断,这里选择 100 作为阈值。

// 将像素点的颜色设置为非黑即白
if (gray > 100) {
  gray = 255
} else {
  gray = 0
}
1
2
3
4
5
6

转换后的图片变化如下:

如果我们打印这个二值化后的图片像素矩阵,可以更加清楚的看到灰色矩形区域几乎都是 0 值。

# 计算滑动距离

对二值化后的图片进行计算,可以非常方便的找到灰色矩形开始的位置,只需判断每个 (x,y) 像素值和前一个像素 (x-1,y)的值变化情况即可。如果某一列出现大量从 255 变化到 0 的像素,这里很大概率是达到了矩形的第一个边界,这列所在的 x 值就是要移动的距离。

// lightArray 存储了二值化后的图片像素信息
// 扫描发现 255->0 变化的最大数量
let maxChangeCount = 0
// maxChangeCount 对应的列值
let index = 0
for (let w = 1; w < width; w++) {
  let changeCount = 0
  for (let h = 0; h < height; h++) {
    if (lightArray[h][w] === 0 && lightArray[h][w - 1] === 255) {
      changeCount++
    }
  }
  if (changeCount > maxChangeCount) {
    maxChangeCount = changeCount
    index = w
  }
}
console.log(`分析得到验证码坐标值:${index}`)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

针对用于测试的熊猫验证码图片,计算得到滑动距离为 127,验证图片发现距离也大概是 127px 左右。

至此,图片滑动验证码自动化破解完成。

# 如果是守方, 如何防止攻击者破解验证码

可以加入拖动轨迹的验证,分析鼠标拖动的轨迹来判断是否是人工拖动。

以下是个简单的例子

<div id="slider"></div>

<script>
  var slider = document.getElementById('slider')
  var isDragging = false
  var trackData = [] // 轨迹数据

  // 添加mousedown或touchstart事件监听
  slider.addEventListener('mousedown', startDragging)
  slider.addEventListener('touchstart', startDragging)

  // 添加mousemove或touchmove事件监听
  document.addEventListener('mousemove', drag)
  document.addEventListener('touchmove', drag)

  // 添加mouseup或touchend事件监听
  document.addEventListener('mouseup', stopDragging)
  document.addEventListener('touchend', stopDragging)

  // 开始拖动
  function startDragging(event) {
    isDragging = true
    trackData = [] // 重置轨迹数据
  }

  // 拖动中
  function drag(event) {
    if (isDragging) {
      // 获取鼠标/触摸坐标
      var x = event.clientX || event.touches[0].clientX
      var y = event.clientY || event.touches[0].clientY

      // 更新滑块的位置
      slider.style.left = x + 'px'
      slider.style.top = y + 'px'

      // 添加当前坐标点到轨迹数据
      trackData.push({ x: x, y: y })
    }
  }

  // 停止拖动
  function stopDragging(event) {
    isDragging = false

    // 在这里发送拖动轨迹数据 到服务端验证
    requestValidateTrack(trackData)
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
上次更新: 2023/07/15, 08:37:32
最近更新
01
JavaScript-test
07-20
02
二维码的原理
07-20
03
利用ChatGPT优化代码
07-20
更多文章>
Theme by Vdoing | Copyright © 2021-2023 XingYun | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式