使用Lua GD库动态生成验证码图片

最近得闲,学习一下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解释器,不知能不能用?

你可能感兴趣的:(lua)