示例
背景
验证码主要是防止机器暴力破解。之前的验证码都是以静态为主,现在一些产品开始使用动态方式,增加破解的难度。动态方式以 gif 最为简单可靠。gif 兼容性好,尺寸小。这里分享的就是一种:用 JS 实现 gif 动态验证码的思路。感谢关注。
任务分解
- 绘制旋转的文字
- 计算每个字符出现位置和角度
- 生成 gif 图片
逐步求精
如何绘制旋转的文字?
了解能用的 API
context.rotate(angle)
使当前坐标系旋转 angle,单位弧度context.translate(x, y)
使当前坐标系偏移 x, y,单位像素context.font
设置字体context.strokeText(text, x, y [, maxWidth ])
给文本描边context.fillText(text, x, y [, maxWidth ])
给文本填充
怎么以文字的中心位置旋转?
1
2
3
4
5
6
7
8
9
10
|
void
function
(
)
{
// ...
var
x
=
100
;
var
y
=
100
;
var
angle
=
1
/
8
*
Math
.
PI
;
context
.
translate
(
x
,
y
)
;
context
.
rotate
(
angle
)
;
context
.
strokeText
(
'A'
,
0
,
0
)
;
// ...
}
(
)
|
以文字的左下角为圆心旋转,不符合预期,见下图效果
本打算做一下偏移的计算,一想到要计算文本中心位置貌似还挺复杂。 还是看看其他人怎么做的,通过关键词 canvas rotate text center
找到一点线索。
1
2
3
4
5
6
|
context
.
save
(
)
;
context
.
translate
(
newx
,
newy
)
;
context
.
rotate
(
-
Math
.
PI
/
2
)
;
context
.
textAlign
=
"center"
;
context
.
fillText
(
"Your Label Here"
,
labelXposition
,
0
)
;
context
.
restore
(
)
;
|
textAlign
是横向对齐,再根据标准找到了一个纵向对齐 textBaseline
1
2
3
4
5
6
7
8
9
10
11
12
|
void
function
(
)
{
// ...
context
.
textAlign
=
'center'
;
// <<<<<<< insert
context
.
textBaseline
=
'middle'
;
// <<<<<<< insert
var
x
=
100
;
var
y
=
100
;
var
angle
=
1
/
8
*
Math
.
PI
;
context
.
translate
(
x
,
y
)
;
context
.
rotate
(
angle
)
;
context
.
strokeText
(
'A'
,
0
,
0
)
;
// ...
}
(
)
|
修改以后,效果符合预期,见下图:
按我的习惯就这种 “常用” 功能就封装成独立函数,方便以后使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
* 绘制旋转的文字
* @param {CanvasRenderingContext2D} context 上下文
* @param {String} text 文本
* @param {Number} x 中心坐标 x
* @param {Number} y 中心坐标 y
* @param {Number} angle 角度,单位弧度
*/
function
rotateText
(
context
,
text
,
x
,
y
,
angle
)
{
if
(
!
context
)
{
return
;
}
context
.
save
(
)
;
// 保存上次的风格设置
context
.
textAlign
=
'center'
;
// 横向居中
context
.
textBaseline
=
'middle'
;
// 纵向居中
context
.
translate
(
x
,
y
)
;
// 修改坐标系原点
context
.
rotate
(
angle
)
;
// 旋转
context
.
strokeText
(
text
,
0
,
0
)
;
// 绘制文本
context
.
restore
(
)
;
// 恢复上次的风格设置
}
|
如何计算每个字符出现位置和角度?
背景文字左右平移 + 旋转,生成随机的字符串计算中心坐标就好了
前景文字基本相似,只要上下来回移动和稍微摇摆,这里用的 cos 曲线控制摇摆。
如何生成 gif 图片
生成 gif 有第三方库可以使用 gifjs。 这里要注意的是,gifjs 用到 worker 技术,所以得在 http://
环境里调试,不能用 file://
环境
注意:由于添加的是同一个 canvas 对象,所以的是使用 copy
模式,将图像数据保留给每一帧。
1
|
gif
.
addFrame
(
canvasTemp
,
{
delay
:
100
,
copy
:
true
}
)
;
|
完整代码
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
|
<!doctype html>
<html>
<head>
<meta
charset
=
"utf-8"
/>
<style>
canvas
{
border
:
black
1px
solid
;
}
</style>
<script src="../library/gif.js"></script>
</head>
<body>
<div>
Key:
<input
type
=
"text"
maxlength
=
"8"
/>
<input
type
=
"button"
value
=
"build"
/>
</div>
<canvas
width
=
"300"
height
=
"70"
>
</canvas>
<img
width
=
"300"
height
=
"70"
/>
<a
download
=
"captcha.gif"
>
download...
</a>
<script>
/**
* 绘制旋转的文字
* @param {CanvasRenderingContext2D} context 上下文
* @param {String} text 文本
* @param {Number} x 中心坐标 x
* @param {Number} y 中心坐标 y
* @param {Number} angle 角度,单位弧度
*/
function
rotateText
(
context
,
text
,
x
,
y
,
angle
)
{
if
(
!
context
)
{
return
;
}
context
.
save
(
)
;
// 保存上次的风格设置
context
.
textAlign
=
'center'
;
// 横向居中
context
.
textBaseline
=
'middle'
;
// 纵向居中
context
.
translate
(
x
,
y
)
;
// 修改坐标系原点
context
.
rotate
(
angle
)
;
// 旋转
context
.
strokeText
(
text
,
0
,
0
)
;
// 绘制文本
context
.
restore
(
)
;
// 恢复上次的风格设置
}
/**
* 随机字符串
* @param{String} chars 字符串
* @param{Number} len 长度
*/
function
randomText
(
chars
,
len
)
{
var
result
=
''
;
for
(
var
i
=
0
;
i
<
len
;
i
++
)
{
result
+=
chars
.
charAt
(
parseInt
(
chars
.
length
*
Math
.
random
(
)
)
)
;
}
return
result
;
}
void
function
(
)
{
// @see http://www.w3.org/TR/2dcontext/
var
canvas
=
document
.
querySelector
(
'canvas'
)
;
var
context
=
canvas
.
getContext
(
'2d'
)
;
context
.
font
=
'30px Verdana'
;
// 字体大小和字体名
var
lineHeight
=
15
;
// 行高
var
backLength
=
3
;
var
backTexts
=
{
}
;
var
backXOffsets
=
{
}
;
var
keyYOffsets
=
{
}
;
var
keyAOffsets
=
{
}
;
var
backSpeed
=
10000
+
parseInt
(
100
*
Math
.
random
(
)
)
;
var
keySpeed
=
12000
+
parseInt
(
100
*
Math
.
random
(
)
)
;
var
key
=
''
;
function
init
(
value
)
{
key
=
String
(
value
)
.
toUpperCase
(
)
;
// 随机备件
for
(
var
i
=
0
;
i
<
canvas
.
height
/
lineHeight
;
i
++
)
{
backTexts
[
i
]
=
randomText
(
'ABCDEFGHIJKLMNOPQRST0123456789'
,
backLength
)
;
backXOffsets
[
i
]
=
Math
.
random
(
)
*
canvas
.
width
;
}
for
(
var
i
=
0
;
i
<
key
.
length
;
i
++
)
{
keyYOffsets
[
i
]
=
Math
.
random
(
)
*
lineHeight
/
2
;
keyAOffsets
[
i
]
=
0.05
-
Math
.
random
(
)
*
0.1
;
}
}
function
renderBack
(
now
,
context
,
text
,
y
,
xOffset
)
{
var
tick
=
now
%
backSpeed
;
for
(
var
i
=
0
;
i
<
backLength
;
i
++
)
{
var
t
=
(
xOffset
+
(
tick
/
backSpeed
)
*
canvas
.
width
+
(
canvas
.
width
/
backLength
)
*
i
)
%
canvas
.
width
;
rotateText
(
context
,
text
[
i
]
,
t
,
y
,
i
/
backLength
*
Math
.
PI
*
2
+
(
tick
/
backSpeed
)
*
Math
.
PI
*
2
)
;
}
}
function
render
(
now
,
context
)
{
context
.
fillStyle
=
'#FFFFFF'
;
context
.
fillRect
(
0
,
0
,
canvas
.
width
,
canvas
.
height
)
;
context
.
fillStyle
=
'#000000'
;
// 绘制背景文字
for
(
var
i
=
0
;
i
<
canvas
.
height
/
lineHeight
;
i
++
)
{
renderBack
(
now
,
context
,
backTexts
[
i
]
,
lineHeight
*
i
,
backXOffsets
[
i
]
)
;
}
// 绘制 key
var
tick
=
now
%
keySpeed
;
var
keyCharWidth
=
canvas
.
width
/
key
.
length
;
for
(
var
i
=
0
;
i
<
key
.
length
;
i
++
)
{
var
tx
=
keyCharWidth
+
(
(
(
canvas
.
width
-
keyCharWidth
)
/
key
.
length
)
*
i
)
%
canvas
.
width
;
var
ty
=
Math
.
cos
(
now
/
1000
)
*
Math
.
PI
*
keyYOffsets
[
i
]
;
rotateText
(
context
,
key
[
i
]
,
tx
,
canvas
.
height
/
2
-
ty
,
Math
.
cos
(
now
/
1000
)
*
Math
.
PI
*
0.1
+
keyAOffsets
[
i
]
)
;
}
}
init
(
'zswang'
)
;
setInterval
(
function
(
)
{
render
(
Number
(
new
Date
)
,
context
)
;
}
,
100
)
;
document
.
querySelector
(
'input[type=text]'
)
.
addEventListener
(
'input'
,
function
(
)
{
init
(
this
.
value
)
;
}
)
;
document
.
querySelector
(
'input[type=button]'
)
.
addEventListener
(
'click'
,
function
(
)
{
var
self
=
this
;
self
.
disabled
=
true
;
var
gif
=
new
GIF
(
{
repeat
:
0
,
workers
:
2
,
quality
:
10
,
workerScript
:
'../library/gif.worker.js'
}
)
;
// 生成 gif 图片
var
canvasTemp
=
document
.
createElement
(
'canvas'
)
;
canvasTemp
.
width
=
canvas
.
width
;
canvasTemp
.
height
=
canvas
.
height
;
var
context
=
canvasTemp
.
getContext
(
'2d'
)
;
context
.
font
=
'30px Verdana'
;
// 字体大小和字体名
context
.
textAlign
=
'center'
;
for
(
var
i
=
0
;
i
<
5000
;
i
+=
100
)
{
render
(
i
,
context
)
;
gif
.
addFrame
(
canvasTemp
,
{
delay
:
100
,
copy
:
true
}
)
;
}
gif
.
on
(
'finished'
,
function
(
blob
)
{
var
url
=
URL
.
createObjectURL
(
blob
)
;
document
.
querySelector
(
'img'
)
.
src
=
url
;
document
.
querySelector
(
'a'
)
.
href
=
url
;
self
.
disabled
=
false
;
}
)
;
gif
.
render
(
)
;
}
)
;
}
(
)
;
</script>
</body>
</html>
|
后记
功能比较简单,也写得比较简单,仅供参考。如果要应用到实战,还有很多细节要考虑
- gif 创建的过程必然得放到后端完成,否则 兼容性、性能、安全性 都是问题(这块和传统的验证过程并无区别)。
- 缓存(背景效果可以重复利用一段时间)。
- 图片大小需要优化,目前是 200K(通过调整帧率和压缩比)。
- 提供方便的调用接口(模块化)。