前端Canvas实现验证码功能

最近在做的项目的登录功能需要通过验证码进行校验,虽然这个功能很常见很常用,但是自己还没有真的手写实现过,于是去网上看了看各个博主的博客,都是推荐通过canvas来实现这个功能,我也看了博主的源码,照着写了一遍,做了一下笔记,由于项目技术栈使用的是vue3+ts,所以下边也是基于此实现的。

页面结构

由于是一个Demo页面,结构很简单,一个输入框用于输入验证码,一个canvas用于显示验证码,一个按钮用于验证是否输入正确,由于技术栈使用了ant-design,所以在模板中有使用其中的组件,可以随意替换为普通的输入框与按钮组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div style="padding: 80px;">
<a-input
style="width: 300px;"
type="text"
v-model:value="inputValue"
placeholder="点击验证码刷新"
/>
<canvas
ref="myCanvas"
@click="refreshCode"
width="120"
height="36"
style="margin: 0 40px;border: 1px solid rgb(136,131,131); border-radius: 4px;"
></canvas>
<a-button
type="primary"
@click="handleSubmit"
>提交</a-button>
</div>
</template>

验证码生成

准备基础数据

首先要将基础的数据准备好,然后运行我们的生成逻辑,所以我在组件挂载之后执行了生成验证码的函数

以下代码均运行在<script setup>中

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
import { ref, onMounted, Ref } from 'vue'

// input 绑定的值
let inputValue = ref<string>('')
// canvas实例
let myCanvas = ref()
// canvas画笔, 挂载之后才能拿到canvas元素
let context: CanvasRenderingContext2D;
// 宽高
let canvas_width: number;
let canvas_height: number;
// 验证码的字符选项
let codeArray: string[] = ['A','B','C','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','W','X','Y','Z','1','2','3','4','5','6','7','8','9','0']
// 显示的验证码
let showNum: string[] = []

onMounted(() => {
// canvas画笔, 挂载之后才能拿到canvas元素
context = myCanvas.value.getContext('2d')
// 宽高
canvas_width = myCanvas.value.clientWidth
canvas_height = myCanvas.value.clientHeight
initDrawCode()

})

入口程序

initDrawCode方法是绘制验证码的一个入口程序,实现如下

initDrawCode方法是绘制验证码的一个入口程序,实现如下

1
2
3
4
5
6
7
8
9
10
11
/**
* @description 绘制验证码的入口
*/
const initDrawCode = (): void => {
// 重新绘制则清空画布
context.clearRect(0,0,canvas_width,canvas_height);
// 依次绘制验证码,线条遮挡,模糊点遮挡
drawText()
drawLine()
drawPoint()
}

绘制过程

在绘制的过程中,画笔颜色是随机生成的,所以先将随机生成颜色的函数放到最前边:

1
2
3
4
5
6
7
8
9
10
/**
* @description 生成随机颜色
* @returns {string}
*/
const randomColor = (): string => {
let r = Math.floor(Math.random() * 256)
let g = Math.floor(Math.random() * 256)
let b = Math.floor(Math.random() * 256)
return `rgb(${r},${g},${b})`
}

验证码的绘制过程中,可以自由控制生成验证码的数量,不过需要注意一下canvas元素的尺寸,要与验证码的数量匹配。

需要注意的一点是,canvas的画笔在旋转角度之后,其坐标轴也会随之旋转,所以我们在将其复位的时候要先复位旋转角度。

验证码绘制过程如下:

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
/**
* @description 绘制验证码
*/
const drawText = (): void => {
// 随机4位验证码
for(let i = 0; i <= 3; i++){
// 取一个codeArray长度内的随机下标
let randomIndex = Math.floor(Math.random() * codeArray.length)
// 根据下标生成一位验证码
showNum[i] = codeArray[randomIndex]
// 产生0-40度的随机角度
let deg = Math.random() * 40 * Math.PI / 180
// 生成文字坐标,初识距离x,y为10和20,文字为24px
let x = 10 + i * 20
let y = 20 + Math.random() * 8
context.font = 'bold 24px 微软雅黑'
context.translate(x, y) // 画笔位置
context.rotate(deg) // 倾斜
// 随机颜色
context.fillStyle = randomColor()
context.fillText(showNum[i], 0, 0)
// 绘制完成后画笔恢复到初识的位置和角度
// 避免下次绘制将位置和角度进行累加
// 重点注意,由于我们之前是先移动,后旋转,在旋转之后canvas的坐标轴被改变了,所以此时我们必须先将旋转角度复原,然后移动到原来的位置
context.rotate(-deg)
context.translate(-x, -y)
}
}

遮挡线与模糊点可以让验证码不那么清晰,增加验证难度,当然这不是必须的。

绘制过程如下:

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
/**
* @description 绘制遮挡线
*/
const drawLine = (): void => {
// 可自行调整线条数量
for (let i = 0; i < 5; i++){
context.strokeStyle = randomColor()
context.beginPath() // 开始绘制
// 随机位置
context.moveTo(Math.random()*canvas_width, Math.random()*canvas_height)
// 画线到随机位置
context.lineTo(Math.random()*canvas_width, Math.random()*canvas_height)
context.stroke()
}
}

/**
* @description 生成模糊点
*/
const drawPoint = (): void => {
// 自由控制模糊点的数量
for(let i = 0; i <= 30; i ++){
context.strokeStyle = randomColor()
context.beginPath()
let x = Math.random() * canvas_width
let y = Math.random() * canvas_height
context.moveTo(x, y)
context.lineTo(x + 1, y + 1)
context.stroke()
}
}

验证与刷新验证码

刷新验证码的操作很简单,再调用一次入口程序即可:

1
2
3
4
5
6
/**
* @description 刷新验证码
*/
const refreshCode = (): void => {
initDrawCode()
}

验证是否输入正确也是很简单的一件事,去除一下空字符然后统一一下大小写就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @description 提交
*/
const handleSubmit = (): void => {
let v = inputValue.value ? inputValue.value.trim() : ''
if(!v){
alert('请输入验证码')
return;
}
if(v.toUpperCase() === showNum.join('').toUpperCase()){
alert('验证成功')
return;
}
alert('验证失败')
}

总结

整个验证码的生成逻辑其实是比较简单的,就是需要对canvas比较熟悉就能很容易的写出来,不过在这个案例中用到的也都是基础功能,没有太大的难度。

真的很推荐typescript,像我这种对canvas几乎一无所知的,只要约束好了类型,也能根据提示来猜个大概。

完整代码防到下边了:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<template>
<div style="padding: 80px;">
<a-input
style="width: 300px;"
type="text"
v-model:value="inputValue"
placeholder="点击验证码刷新"
/>
<canvas
ref="myCanvas"
@click="refreshCode"
width="120"
height="36"
style="margin: 0 40px;border: 1px solid rgb(136,131,131); border-radius: 4px;"
></canvas>
<a-button
type="primary"
@click="handleSubmit"
>提交</a-button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, Ref } from 'vue'

// input 绑定的值
let inputValue = ref<string>('')
// canvas实例
let myCanvas = ref()
// canvas画笔, 挂载之后才能拿到canvas元素
let context: CanvasRenderingContext2D;
// 宽高
let canvas_width: number;
let canvas_height: number;
// 验证码的字符选项
let codeArray: string[] = ['A','B','C','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','W','X','Y','Z','1','2','3','4','5','6','7','8','9','0']
// 显示的验证码
let showNum: string[] = []

onMounted(() => {
// canvas画笔, 挂载之后才能拿到canvas元素
context = myCanvas.value.getContext('2d')
// 宽高
canvas_width = myCanvas.value.clientWidth
canvas_height = myCanvas.value.clientHeight
initDrawCode()

})

/**
* @description 绘制验证码的入口
*/
const initDrawCode = (): void => {
// 重新绘制则清空画布
context.clearRect(0,0,canvas_width,canvas_height);
// 依次绘制验证码,线条遮挡,模糊点遮挡
drawText()
drawLine()
drawPoint()
}
/**
* @description 绘制验证码
*/
const drawText = (): void => {
// 随机4位验证码
for(let i = 0; i <= 3; i++){
// 取一个codeArray长度内的随机下标
let randomIndex = Math.floor(Math.random() * codeArray.length)
// 根据下标生成一位验证码
showNum[i] = codeArray[randomIndex]
// 产生0-40度的随机角度
let deg = Math.random() * 40 * Math.PI / 180
// 生成文字坐标,初识距离x,y为10和20,文字为24px
let x = 10 + i * 20
let y = 20 + Math.random() * 8
context.font = 'bold 24px 微软雅黑'
context.translate(x, y) // 画笔位置
context.rotate(deg) // 倾斜
// 随机颜色
context.fillStyle = randomColor()
context.fillText(showNum[i], 0, 0)
// 绘制完成后画笔恢复到初识的位置和角度
// 避免下次绘制将位置和角度进行累加
// 重点注意,由于我们之前是先移动,后旋转,在旋转之后canvas的坐标轴被改变了,所以此时我们必须先将旋转角度复原,然后移动到原来的位置
context.rotate(-deg)
context.translate(-x, -y)
}
}

/**
* @description 绘制遮挡线
*/
const drawLine = (): void => {
// 可自行调整线条数量
for (let i = 0; i < 5; i++){
context.strokeStyle = randomColor()
context.beginPath() // 开始绘制
// 随机位置
context.moveTo(Math.random()*canvas_width, Math.random()*canvas_height)
// 画线到随机位置
context.lineTo(Math.random()*canvas_width, Math.random()*canvas_height)
context.stroke()
}
}

/**
* @description 生成模糊点
*/
const drawPoint = (): void => {
// 自由控制模糊点的数量
for(let i = 0; i <= 30; i ++){
context.strokeStyle = randomColor()
context.beginPath()
let x = Math.random() * canvas_width
let y = Math.random() * canvas_height
context.moveTo(x, y)
context.lineTo(x + 1, y + 1)
context.stroke()
}
}

/**
* @description 生成随机颜色
* @returns {string}
*/
const randomColor = (): string => {
let r = Math.floor(Math.random() * 256)
let g = Math.floor(Math.random() * 256)
let b = Math.floor(Math.random() * 256)
return `rgb(${r},${g},${b})`
}

/**
* @description 刷新验证码
*/
const refreshCode = (): void => {
initDrawCode()
}

/**
* @description 提交
*/
const handleSubmit = (): void => {
let v = inputValue.value ? inputValue.value.trim() : ''
if(!v){
alert('请输入验证码')
return;
}
if(v.toUpperCase() === showNum.join('').toUpperCase()){
alert('验证成功')
return;
}
alert('验证失败')
}
</script>
作者

胡兆磊

发布于

2022-03-24

更新于

2022-10-23

许可协议