Canvas实现前端验证码

一、前言

canvas技术允许我们通过javascript脚本来绘制html页面,通过canvas我们可以绘制简单图形甚至开发简单游戏。本文主要讲述如何通过canvas技术实现前端验证码功能

源码在本文最后,需要的自取

思路:

通过观察验证码,发现验证码主要包括三部分,背景画布、验证码、干扰线和干扰点,只需要实现这三部分就可以完成这个功能

遇到比较棘手的问题,应该学会如何去拆解问题,将大问题变成一堆小问题,再将小问题拆解成更细小的问题,你就会发现问题迎刃而解

二、实现

2.1 生成随机背景图

首先是html部分,只需要一个canvas画布

1
<canvas id="canvas" width="240" height="76"></canvas>

绘制背景图就是在这块空白的canvas上绘制一个矩形作为背景,因此我们只需要生成一个随机的颜色即可,而颜色是由三原色组成,也就是RGB,范围是0-255,因此我们只需要生成三个随机数作为RGB的三个值就能生成一个随机颜色

生成随机数和生成随机颜色我们会经常用到,因此写成公用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 产生一个从min到max的随机数
* @param {Object} min 最小值
* @param {Object} max 最大值
*/
function randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
}

/**
* 生成随机颜色rgb字符串
* 返回rgb(0,0,0)这种格式
* @param {Object} minColor 颜色取值的开始
* @param {Object} maxColor 颜色取值的结束
*/
function randomRGBColor(minColor, maxColor) {
return `rgb(${randomNum(minColor, maxColor)},${randomNum(minColor, maxColor)},${randomNum(minColor, maxColor)})`
}

我这里返回的字符串用了ES6字符串模板的语法,使用字符串拼接也可以实现

然后开始绘制,颜色我取的从140开始,因为太小的颜色会特别暗,容易看不清楚,这个可以自己尝试

1
2
3
4
5
6
7
8
9
10
11
/**
* 绘制背景色
* @param {Object} ctx canvas上下文
*/
function drawBg(ctx) {
let color = randomRGBColor(140, 240);
// 设置画笔的颜色
ctx.fillStyle = color;
// 绘制矩形
ctx.fillRect(0, 0, 240, 76);
}

2.2 生成随机验证码

验证码都是随机字母和数字,我们只需要定义一个数组,数组中是26个字母和数字,通过产生一些随机索引就可以产生随机字符和数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 产生len位随机验证码
* @param {Object} len 随机字符的位数
*/
function randomCode(len) {
let code = "";
let arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0
]
for (let i = 0; i < len; i++) {
// 随机索引
let randomIndex = randomNum(0, arr.length - 1);
code += arr[randomIndex];
}
return code;
}

2.3 绘制验证码

在开始绘制之前有两个canvasapi需要解释一下,分别是saverestore,这两个方法分别用来保存画笔状态和还原画笔状态,这个画笔就是canvas上下文,后面简称为画笔

比如我一共绘制六个验证码,整个canvas画布一共240像素,相当于每个验证码占据60像素,每次循环都通过循环变量i控制canvas在对应格内随机生成一个位置来绘制字符

绘制原理

循环的过程如下,建议结合代码食用

  • 通过save保存canvas画笔初始状态(比如画笔颜色、位置等属性)
  • 为画笔生成随机字号,画笔颜色
  • 将画笔移动到对应中,并在对应内随机生成一个位置(如上图)
  • 随机将上下文旋转一个角度
  • 通过fillText绘制文本
  • 还原画笔(将画布还原到原来位置,还原颜色,字号,旋转情况等等)
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
/**
* 绘制验证码过程
* @param {Object} ctx 画笔
* @param {Object} text 验证码
*/
function drawText(ctx, text) {
for (let i = 0; i < text.length; i++) {
// 先保存一下画笔, 绘制完这个字符在重置画笔
ctx.save();
// 随机生成验证码字号
ctx.font = randomNum(24, 40) + "px serif";
// 随机生成一个画笔颜色
let color = randomRGBColor(85, 240);
ctx.fillStyle = color;
// 随机坐标, 通过控制画布移动来控制每个字符在不同位置
const posX = randomNum(40 * i + 20, 40 * (i + 1) - 20);
const posY = randomNum(30, 70)
ctx.translate(posX, posY);
// 随机生成字符旋转角度
let randomRotate = randomNum(-30, 30)
ctx.rotate((Math.PI / 180) * randomRotate)
// 因为画布已经移动, 因此原地画字符即可
ctx.fillText(text[i], 0, 0)
// 还原画笔, 将画笔设置还原, 比如平移, 旋转, 颜色设置等等
ctx.restore();
}
}

2.4 绘制干扰线和干扰点

这部分比较简单,干扰点就是生成随机位置绘制半径很小的圆形,干扰线就是随机生成几个坐标,绘制直线

直接上代码吧,这部分没什么需要解释的

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
/**
* 绘制干扰圆点
* @param {Object} ctx 画笔
* @param {Object} num 干扰点个数,默认值为20
*/
function paintDot(ctx, num = 20) {
for (let i = 0; i < num; i++) {
// 随即背景色
ctx.fillStyle = randomRGBColor(85, 255);
ctx.beginPath();
// 随机位置x, y
let dotX = randomNum(2, 238);
let dotY = randomNum(2, 74);
ctx.arc(dotX, dotY, 1, 0, 2 * Math.PI)
ctx.fill();
ctx.closePath();
}
}


/**
* 绘制干扰线
* @param {Object} ctx 画笔
* @param {Object} num 干扰线条数,默认值为5
*/
function paintLine(ctx, num = 5) {
for (let i = 0; i < num; i++) {
ctx.strokeStyle = randomRGBColor(85, 235);
// 线宽
ctx.lineWidth = 2
ctx.beginPath();
// 线的起点
ctx.moveTo(randomNum(0, 240), randomNum(0, 76));
// 线的重点
ctx.lineTo(randomNum(0, 240), randomNum(0, 76));
// 填充颜色
ctx.stroke();
ctx.closePath();
}
}

2.5 整体流程

canvas画布上定义一个点击函数

1
<canvas id="canvas" width="240" height="76" onclick="paint()"></canvas>

paint方法就是执行上面那些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function paint() {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 绘制验证码背景
drawBg(ctx);
// 生成随机字符串
let code = randomCode(6);
// 绘制验证码
drawText(ctx, code);
// 绘制干扰圆点
paintDot(ctx, 40);
// 绘制干扰线
paintLine(ctx, 6)
}

然后在js脚本第一行执行这个方法即可

三、源码参考

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
155
156
157
158
159
160
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>canvas</title>
<style>
* {
margin: 0;
padding: 0;
}

canvas {
display: block;
margin: 30px auto;
cursor: pointer;
overflow: hidden;
}

.text {
width: 300px;
text-align: justify;
}
</style>
</head>
<body>
<canvas id="canvas" width="240" height="76" onclick="paint()"></canvas>
<script>
paint();

/**
* 绘制过程
*/
function paint() {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 绘制验证码背景
drawBg(ctx);
// 生成随机字符串
let code = randomCode(6);
// 绘制验证码
drawText(ctx, code);
// 绘制干扰圆点
paintDot(ctx, 40);
// 绘制干扰线
paintLine(ctx, 6)
}

/**
* 产生一个从min到max的随机数
* @param {Object} min 最小值
* @param {Object} max 最大值
*/
function randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
}

/**
* 产生len位随机验证码
* @param {Object} len 随机字符的位数
*/
function randomCode(len) {
let code = "";
let arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0
]
for (let i = 0; i < len; i++) {
let randomIndex = randomNum(0, arr.length);
code += arr[randomIndex];
}
return code;
}

/**
* @param {Object} ctx 画笔
* @param {Object} text 验证码
*/
function drawText(ctx, text) {
for (let i = 0; i < text.length; i++) {
// 先保存一下画笔, 绘制完这个字符在重置画笔
ctx.save();
// 随机生成验证码字号
ctx.font = randomNum(24, 40) + "px serif";
// 随机生成一个画笔颜色
let color = randomRGBColor(85, 240);
ctx.fillStyle = color;
// 随机坐标, 通过控制画布移动来控制每个字符在不同位置
const posX = randomNum(40 * i + 20, 40 * (i + 1) - 20);
const posY = randomNum(30, 70)
ctx.translate(posX, posY);
// 随机生成字符旋转角度
let randomRotate = randomNum(-30, 30)
ctx.rotate((Math.PI / 180) * randomRotate)
// 因为画布已经移动, 因此原地画字符即可
ctx.fillText(text[i], 0, 0)
// 还原画笔, 将画笔设置还原, 比如平移, 旋转, 颜色设置等等
ctx.restore();
}
}

/**
* 绘制干扰圆点
* @param {Object} ctx 画笔
* @param {Object} num 干扰点个数,默认值为20
*/
function paintDot(ctx, num = 20) {
for (let i = 0; i < num; i++) {
// 随即背景色
ctx.fillStyle = randomRGBColor(85, 255);
ctx.beginPath();
// 随机位置x, y
let dotX = randomNum(2, 238);
let dotY = randomNum(2, 74);
ctx.arc(dotX, dotY, 1, 0, 2 * Math.PI)
ctx.fill();
ctx.closePath();
}
}

/**
* 绘制干扰线
* @param {Object} ctx 画笔
* @param {Object} num 干扰线条数,默认值为5
*/
function paintLine(ctx, num = 5) {
for (let i = 0; i < num; i++) {
ctx.strokeStyle = randomRGBColor(85, 235);
// 线宽
ctx.lineWidth = 2
ctx.beginPath();
// 线的起点
ctx.moveTo(randomNum(0, 240), randomNum(0, 76));
// 线的重点
ctx.lineTo(randomNum(0, 240), randomNum(0, 76));
// 填充颜色
ctx.stroke();
ctx.closePath();
}
}

/**
* 生成随机颜色rgb字符串
* @param {Object} minColor
* @param {Object} maxColor
*/
function randomRGBColor(minColor, maxColor) {
return `rgb(${randomNum(minColor, maxColor)},${randomNum(minColor, maxColor)},${randomNum(minColor, maxColor)})`
}

/**
* 绘制背景色
* @param {Object} ctx
*/
function drawBg(ctx) {
let color = randomRGBColor(140, 240);
ctx.fillStyle = color
ctx.fillRect(0, 0, 240, 76);
}
</script>
</body>
</html>