最近得闲,学习一下Lua。
Lua下有个gd图形库,通过简单的Lua语句就能控制、生成图片。
之前在某个项目中要用到验证码,当时对这方面不太了解,就采用最不专业的做法:预先准备好若干验证码图片,把对应的值存入到数据库;使用时随机取出一对“图片-验证码值”供用户验证。这样做的好处是减少编码复杂度和服务器负担,但是问题也显而易见:预先准备的验证码图片数量有限,要是有人恶意攻击的话,这种验证码恐怕只是个摆设。要是专业人士见到我的这种实现,只怕会笑掉大牙。
当时也考虑过动态生成图片,随机生成几个数字、字母组成的验证码,然后将此验证码生成图片,最后对验证码图片进行模糊处理(倾斜、模糊、加干扰等)。验证码的生成到不难,有比较成熟的随机函数可以做到;但是将验证码生成图片并做模糊处理,当时就没有什么好的办法了。考虑过Java下应该有图形库可以做到这一点,但是觉得代码量肯定不小,运行效率恐怕也难以保证,所以就没深究。
直到最近接触到Lua的gd库,才重新想起这事。关于该gd库的细节,请参看官方文档:http://lua-gd.luaforge.net/manual.html
不说废话,直接上代码:
1
require
(
"
gd
"
)
2
3
--
定义词典
4
5
dict
=
{
'
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
'
,
'
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
'
}
6
7
--
随机种子
8
9
math.randomseed
(
os.time
())
10
11
im2
=
gd.createTrueColor(
100
,
40
)
12
13
white
=
im2:colorAllocate(
255
,
255
,
255
)
14
15
stringmark
=
""
16
17
for
i
=
1
,
6
do
18
19
stringmark
=
stringmark..dict[
math.random
(
62
)]
20
21
end
22
23
im2:string(gd.FONT_GIANT,
18
,
10
, stringmark, white)
24
25
im2:png(
"
./output/验证码.png
"
,
100
)
26
27
这样总共用不到20行代码就实现了一个简单的验证码,效果如下:
虽然基本实现了验证码图片的生成,但还不太理想;要实现真正可用的验证码,大概还需要做如下处理:设置不同字体、字符要随机倾斜、要随机模糊字符、要增加干扰等。
因此在此基础上略作了改进:设置不同字体、字符随机倾斜;至于随机模糊字符、增加干扰暂时还没想好怎么处理。最后代码如下:
1
require
(
"
gd
"
)
2
3
require
(
"
lfs
"
)
4
5
--
定义词典
6
7
dict
=
{
'
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
'
,
'
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
'
}
8
9
--
随机种子
10
11
math.randomseed
(
os.time
())
12
13
im2
=
gd.createTrueColor(
100
,
40
)
14
15
white
=
im2:colorAllocate(
255
,
255
,
255
)
16
17
stringmark
=
""
18
19
fonts
=
{}
20
21
--
查找字体
22
23
function
searchFont()
24
25
local
i
=
1
26
27
for
file
in
lfs.dir(
"
./复件 output/
"
)
do
28
29
if
file
~=
"
.
"
and
file
~=
"
..
"
then
30
31
fonts[i]
=
string.sub
(file,
1
,
string.find
(file,
"
ttf
"
))..
"
tf
"
32
33
print
(fonts[i])
34
35
i
=
i
+
1
36
37
end
38
39
end
40
41
end
42
43
--
测试不同字体的效果
44
45
function
testFont()
46
47
searchFont()
48
49
if
table.getn(fonts)
==
0
then
--
没有指定字体路径,就搜索系统字体
50
51
for
file
in
lfs.dir(
"
C:/WINDOWS/Fonts/
"
)
do
52
53
if
string.find
(file,
"
.ttf
"
)
and
not
string.find
(file,
"
esri
"
)
then
54
55
makeStringWithRotate(file)
56
57
end
58
59
end
60
61
else
--
否则就使用指定字体
62
63
for
i
=
1
,table.getn(fonts)
do
64
65
makeStringWithRotate(fonts[i])
66
67
end
68
69
end
70
71
end
72
73
--
生成带角度字符串
74
75
function
makeStringWithRotate(font)
76
77
for
i
=
1
,
6
do
78
79
local
s
=
dict[
math.random
(
62
)]
80
81
im2:stringFT(white,
"
C:/WINDOWS/Fonts/
"
..font,
18
,
math.random
()
/
math.pi
,
5
+
(i
-
1
)
*
15
,
25
, s)
82
83
stringmark
=
stringmark..s
84
85
end
86
87
im2:png(
"
./output/
"
..font..
"
.png
"
,
100
)
88
89
--
清理工作,准备下次使用
90
91
stringmark
=
""
92
93
im2
=
gd.createTrueColor(
100
,
40
)
94
95
end
96
97
--
生成普通字符串
98
99
function
makeString()
100
101
im2
=
gd.createTrueColor(
100
,
40
)
102
103
white
=
im2:colorAllocate(
255
,
255
,
255
)
104
105
for
i
=
1
,
6
do
106
107
stringmark
=
stringmark..dict[
math.random
(
62
)]
108
109
end
110
111
im2:string(gd.FONT_GIANT,
18
,
10
, stringmark, white)
112
113
stringmark
=
""
114
115
im2:png(
"
./output/验证码.png
"
,
100
)
116
117
end
118
119
testFont()
120
121
--
makeString()
122
123
说明如下:
由于不同字体的显示效果不一样,在有些字体中0、o、O不分;i、I、1、l、L不分;有些字体无法显示;这样导致验证码无法识别,因此必须去掉不适合用来生成验证码的字体。但是由于系统字体太多,如果逐一由手工验证,将是一件繁复而无意义的工作,因此在这里我采用“循环验证”的方式来处理:
1.使用系统中每一种字体都生成一张验证码图片放到指定目录A中("./output/"),图片名即字体名
2.依次对这些验证码图片进行验证,剔除不适合做验证码的字体
3.将剔除后合格的验证码图片拷到指定目录B下(C:/luaaio_2.0_windows/test/test_gd/复件 output/),删除原目录A("./output/")中的内容
4.重新运行本程序,将读取目录B中合格字体,然后使用这些字体创建验证码图片到目录A中
5.重复步骤2,继续剔除不合格的字体,直到得到所有合格的字体。
最后,在我自己系统上经过多次运行,最后从几百个字体中得到比较容易分辨、适合作验证码的字体如下:
courbd.ttf
courbi.ttf
DejaVuMonoSans.ttf
DejaVuMonoSansBold.ttf
DejaVuMonoSansBoldOblique.ttf
DejaVuMonoSansOblique.ttf
lucon.ttf
monosbi.ttf
nina.ttf
simhei.ttf
simkai.ttf
swissci.ttf
tahomabd.ttf
timesbd.ttf
timesbi.ttf
timesi.ttf
trebuc.ttf
trebucit.ttf
效果如下:
说明:上面代码中为了访问文件系统,使用了lua扩展“LuaFileSystem”,具体些请参看文档:http://keplerproject.github.com/luafilesystem/index.html。
现在找出合适的字体了,做进一步改进:每次生成使用随机字体。
加上背景颜色,代码如下:
1
require
(
"
gd
"
)
2
3
require
(
"
lfs
"
)
4
5
--
定义词典
6
7
dict
=
{
'
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
'
,
'
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
'
}
8
9
--
随机种子
10
11
math.randomseed
(
os.time
()
*
2
-
1023
)
12
13
im2
=
gd.createTrueColor(
100
,
40
)
14
15
fg
=
im2:colorAllocate(
129
,
32
,
28
)
16
17
bg
=
im2:colorAllocate(
216
,
235
,
238
)
18
19
FONT_PATH
=
"
C:/WINDOWS/Fonts/
"
20
21
fonts
=
{
"
courbd.ttf
"
,
"
courbi.ttf
"
,
"
DejaVuMonoSans.ttf
"
,
"
DejaVuMonoSansBold.ttf
"
,
"
DejaVuMonoSansBoldOblique.ttf
"
,
"
DejaVuMonoSansOblique.ttf
"
,
"
lucon.ttf
"
,
"
monosbi.ttf
"
,
"
nina.ttf
"
,
"
simhei.ttf
"
,
"
simkai.ttf
"
,
"
swissci.ttf
"
,
"
tahomabd.ttf
"
,
"
timesbd.ttf
"
,
"
timesbi.ttf
"
,
"
timesi.ttf
"
,
"
trebuc.ttf
"
,
"
trebucit.ttf
"
}
22
23
--
生成的随机码
24
25
stringmark
=
""
26
27
--
初始化:创建图片、设置背景
28
29
function
init()
30
31
im2
=
gd.createTrueColor(
100
,
40
)
32
33
im2:filledRectangle(
0
,
0
,
100
,
40
,bg)
34
35
stringmark
=
""
36
37
end
38
39
--
查找字体
40
41
function
searchFont()
42
43
local
i
=
1
44
45
for
file
in
lfs.dir(
"
./复件 output/
"
)
do
46
47
if
file
~=
"
.
"
and
file
~=
"
..
"
and
file
~=
"
Thumbs.db
"
then
48
49
fonts[i]
=
string.sub
(file,
1
,
string.find
(file,
"
ttf
"
))..
"
tf
"
50
51
print
(fonts[i])
52
53
i
=
i
+
1
54
55
end
56
57
end
58
59
end
60
61
--
测试不同字体的效果
62
63
function
testFont()
64
65
searchFont()
66
67
if
table.getn(fonts)
==
0
then
--
没有指定字体路径,就搜索系统字体
68
69
for
file
in
lfs.dir(FONT_PATH)
do
70
71
if
string.find
(file,
"
.ttf
"
)
and
not
string.find
(file,
"
esri
"
)
then
72
73
makeStringWithRotate(file)
74
75
end
76
77
end
78
79
else
--
否则就使用指定字体
80
81
for
i
=
1
,table.getn(fonts)
do
82
83
makeStringWithRotate(fonts[i])
84
85
end
86
87
end
88
89
end
90
91
--
生成带角度字符串
92
93
function
makeStringWithRotate(font)
94
95
for
i
=
1
,
6
do
96
97
local
s
=
dict[
math.random
(
62
)]
98
99
im2:stringFT(white,FONT_PATH..font,
18
,
math.random
()
/
math.pi
,
5
+
(i
-
1
)
*
15
,
25
, s)
100
101
stringmark
=
stringmark..s
102
103
end
104
105
im2:png(
"
./output/
"
..font..
"
.png
"
,
100
)
106
107
--
清理工作,准备下次使用
108
109
stringmark
=
""
110
111
im2
=
gd.createTrueColor(
100
,
40
)
112
113
end
114
115
--
生成普通字符串
116
117
function
makeString()
118
119
im2
=
gd.createTrueColor(
100
,
40
)
120
121
white
=
im2:colorAllocate(
255
,
255
,
255
)
122
123
for
i
=
1
,
6
do
124
125
stringmark
=
stringmark..dict[
math.random
(
62
)]
126
127
end
128
129
im2:string(gd.FONT_GIANT,
18
,
10
, stringmark, white)
130
131
stringmark
=
""
132
133
im2:png(
"
./output/验证码.png
"
,
100
)
134
135
end
136
137
--
使用随机字体生成带角度的字符串
138
139
function
makeIt()
140
141
makeStringWithRotate(fonts[
math.random
(
18
)])
142
143
end
144
145
--
调用接口:直接使用下面的三个函数(之一),即可得到不同类型的验证码
146
147
makeIt()
148
149
--
testFont()
150
151
--
makeString()
152
153
效果如下:
奇怪的是:在makeIt()中,math.random(18)在似乎在一定时间内返回固定值,可能是因为使用的是系统时间做随机种子。
这个程序还有如下不足:
(1)没有加“字符模糊”和“干扰”,还是比较容易被破解。这个目前还没什么好的思路,暂时就不考虑。
(2)由于验证码只是简单的采用系统时间作为随机数种子生成的,是一种伪随机数。如果通过某种方式得到这个随机数的种子(即系统时间),然后根据相同的随机数算法,是完全可以计算出验证码的:这样验证码就不攻自破了。因此在这里可以考虑对这个随机数种子做进一步处理,将系统时间按照某种算法进行处理,得到的结果作为随机数种子;这样即使得到了系统时间,如果不知道这个算法,同样得不到最后的随机数种子。据我估计:这个算法应该不需要很复杂,即使作简单的四则运算,再配合随机数算法,应该是不那么容易破解的。
(3)还可以对验证码图片的随机性作进一步改进:字符颜色、背景颜色都可以随机,不过这样可能搭配出来的颜色会看不清(但可以预定义一组颜色进行处理)。不过没什么价值,也就罢了。
关于验证码,我没有作专门研究:不知道在实际应用中的验证码是如何实现的,也不清楚到底需要考虑哪些问题。这里仅仅是我学习Lua的gd库时的一个练习,没有在实际生产系统中检验过,有什么问题欢迎指正。
--------
ps:关于增强验证码的随机性,觉得还可以改进:在这里预先定义的字典都是固定的,按照“小写字母-大写字母-数字”的顺序排列,攻击者如果得到随机种子和这个字典顺序,按照相同的随机算法,是可以计算出验证码的。要解决这个问题,就得从这三个因素下手:
(1)对随机算法保密,即使得到随机种子和词典,也无法计算出验证码。这是最理想的方法,但很难实现:大家用的都是lua库中的随机库,除非自己对随即算法做改进,并且保密起来。
(2)对随机种子保密,前面已经提到过。
(3)对词典保密。这里采用的是固定的词典,按字母顺序排列,很容易就被猜到。要改进的话,可以在每次调用时初始化词典,采用随机算法将词典顺序打乱;这样每次调用时采用的都是随机词典,也会增加破解难度。问题是这样一来,计算这个随机词典也需要额外开销,对整个系统来说是不是合算,也需要考虑;而且,如果生成随机词典的种子被人得到,随机词典也会被人破解。
最近想用php写个网站,想把这套验证码搬上去,不知是否可行。对php不是很了解,听说php有个扩展实现了lua解释器,不知能不能用?