服务的防盗门:验证码!

 

老王带你去打码~...



又到周日了,时间真是过的好快,就跟脱缰的野马一样,拼命的往前跑。今天我们聊的话题呢,也跟马有关,不过这个码和那个马不一样,他叫验证码。



说到验证码,大家到百度上随便一搜,就能搜到不计其数的资料,说明这个技术已经十分之成熟。那老王为什么要讲他呢,其实是因为,老王觉得他很有意思,并且老王觉得自己可以讲出不一样的东西。怎么样,跟老王一起来吧~

验证码属于防攻击和反作弊体系的一部分,用来保证我们的服务稳定和可靠,防止被恶意攻击(包括行为的攻击和内容的骚扰),就跟防盗门一样,保护着我们服务的安全。常常和用户行为系统、内容过滤系统一起来协同工作。比如:

1、如果行为系统发现某一个ip一直高频的访问某一个或几个页面,我们就可以出验证码,去判断他是否是爬虫;

2、如果行为系统发现突然大量的用户访问集中到达(DDoS攻击),就可以对用户出验证码,将真正的用户和肉鸡分开(谷歌原来时不时就会出验证码,用来判断是否是真实用户。不过要做防DDoS攻击,除了这个手段还有其他的工作和要求,有机会跟大家一起详细聊聊);

3、如果内容过滤系统发现用户提交的内容(比如:贴子、问答等)有问题,可以出验证码用于甄别是否是真正的用户。

所以,验证码在我们的防御体系中扮演着很重要的角色。

那为啥验证码历经了这十多年还经久不衰呢?其中一个最重要的原因,就是他性价比很高:实现简单,BUT识别太难! 他就像是险峻的隘口,易守难攻。

好了,背景聊的差不多了,我们就来聊聊验证码的实现吧~

验证码的种类:

验证码在实现上有很多方法,大体上可以归结为以下几类:

1、枚举问答式:提前生成一些问题,让用户输入答案。这种一般是中文显示,在识别上比较困难。不过因为是枚举的,如果抓取足够多的样本,就有可能获取这个有限集的结果,从而被一定概率的破解。



2、运算表达式:通过加减乘除运算,产生计算表达式,让用户填写计算结果。这种不是固定集合,不过因为一般会控制在两位数内的运算,所以整个集合也是比较有限的,所以也是全靠验证码自身的变换程度来防止破解。



3、图像和文字的关联:这种验证码通过让用户识别文字然后选图案来做到验证的效果。某火车票网站就用过这种方式。



4、字符拆分和组合:通过提供一个整体文字组合,让用户选择单独的字符顺序,来提升验证码的复杂程度。这种验证码相当于要进行多次字符识别,防盗效果比较好,且不是有限集合。同时,在手机上点选效果比输入效果好。贴吧就用了这种验证码。



5、最常见的随机字符验证码:这个是我们见的最多,用的最多,实现最多……的一种验证码。他实现简单,且随机组合,只要实现的好,就能很大程度上防止破解。



6、gif验证码:曾经某火车票网站还出过gif图片的验证码。就是将第5种方法,生成多张图,然后合并成一张动态gif图,让用户输入。虽然这种方法,让真正的用户苦不堪言(还没看清楚就变了),但是对机器来讲却是好事一桩,因为同一个字符组合可以有多张图片来识别,将识别准确率大大提高。后来,这个验证码,就消失了……

好了,以上几种验证码就是我们比较常见的。今天老王就拿最最最常见的那个验证码来讲讲,如何实现。

验证码图像的实现:

之前看过老王关于图像文章的朋友,大体都知道一点关于图像识别的算法。一般步骤:灰度->二值化->切割->匹配等。所以要让验证码不容易识别,我们就需要从上述几个手段来增加难度。

1、灰度:就是将一张彩色图变成灰度图,这个算法一般比较固定,不太依赖环境,所以基本不在这个步骤动手脚。但是,下一个步骤有可能依赖这个步骤。我们先往下看。

2、二值化:将灰度图编程纯的黑白图(只有黑和白两种颜色)。要想干扰二值化,就要尽量让前景色和背景色很接近,使得二值化的时候,很难找到区分前景和背景的那个中间值。所以,我们一般在彩色化验证码的时候,将某些字符的颜色尽量靠近背景色。这样,二值化的时候,就可能将某些字符识别成背景色。或者,让背景色有些渐变,让人眼能区分而机器却不好区分前背景。

3、切割:就是将从背景中抽离出来的前景图案,切成一个个的字符。在这个阶段,我们就可以做很多工作来阻止程序的切割。比如:我们将字符与字符做粘连、增加干扰线、将字符在一定范围内随机摆放等等。这就使得切割程序很难有效的判断字符的范围。

4、匹配:匹配就是将切割好的字符,用一定算法和已知的字符图案拟合的手法。如果我们产生的图像和识别者的样本图像很像,那这个匹配难度就很低。所以,我们在这个阶段,就需要加入尽可能多的手段,去干扰这种匹配。可做的工作也很多,比如:让字符扭曲、旋转等,增加字符被拟合的难度。

所以,我们要实现一个比较好的验证码,一般要都包含以下几个要素:

1、背景色和前景色混淆;

2、背景干扰线或干扰图;

3、字符集合尽量扩大,比如:要用户输入4-6个不固定的字符,每个字符大体上有26个字母+10个数字(有可能要去掉一些人眼都容易混淆的组合,比如l、1、I;0、O、o;b、6;g、q、9等等),这样总的可能性就大概是304-306种可能

4、字符的旋转;

5、字符的扭曲;

6、字符的粘连。

对应不同语言,都有自己的图形库(比如c的gd图形库,其他一些动态语言也引用了这个gd库;java自己的java.awt库等等)。这些库都能实现对应的效果(百度一下,算法遍地)。只是在实现的时候,注意以下几点:

1、字符扭曲变形的时候,需要进行平滑(减少锯齿);

2、产生随机数的时候,一定有加盐的种子,避免被人利用随机算法攻击;

3、避免相似字符的出现,减少正常用户的误伤;

4、前背景色混淆的时候,也不要太难看,影响用户的正常体验。

验证码验证的实现:

上面我们介绍了图像的生成,接下来我们聊聊如何来验证。

我们验证的过程一般如下:

1、先用随机算法产生一个4-6个字符的随机串:string captcha_code;

2、随机串对用户隐藏;

3、我们把图片发给用户;

4、让用户输入一个字符串:string input_code;

5、用captcha_code和input_code比较,看是否相同(有时候可以忽略大小写)。

以上几步中,第1、3、4、5步相对都比较好实现,关键的是第2步,如何实现captcha_code对用户隐藏,而对我们的系统可以方便的获取?

老王总结了一些方法:

1、把captcha_code放入到session,用户提交input_code时,用cookie取出对应session中的captcha_code。这种方法实现简单,如果服务器只有一台,就非常好。不过,如果是分布式系统,就需要做session同步,用起来很麻烦;

2、把captcha_code放入到分布式cache系统中。这个是对上一种方法的改进。既然分布式的session同步很难,那么我就放到一个集中式的系统中去管理,这样便能方便的存取这个数据;

3、将captcha_code做可逆加密,将加密的密文串encrypt_code放入到用户的cookie中。用户提交结果的时候,从cookie中取出encrypt_code做解密操作,然后和input_code做对比。这种方式,需要注意加盐,避免多次请求后出现相同的加密结果。另外,因为密文和图像都是放到客户端的,所以,要防止重放攻击。手段也是在做加密的时候,加入类似user_id、request_ip、timestamp等信息,限制encrypt_code的使用范围、使用时间等;

4、将captcha_code做不可逆的加密(比如md5、sha摘要等),将密文串signed_code放到用户的cookie。用户提交的时候,将input_code做同样的加密操作,然后和cookie中的signed_code做对比。这种方法和上面一种方法一样,要注意重复和重放攻击。

所以,对于第1种方案,非常适合单机服务器;第2-4种方案,可以适用于分布式的系统中。第2种方案服务器存储了状态;第3、4种方案,状态是放到客户端的,降低了服务器的实现难度,但是也增加了被攻击的风险。因此各有利弊。

好了,以上的内容,就是老王对验证码的理解。老王自己也实现了一个验证码(在网上找的图形库操作代码改的,不过设计思想是尽量符合我们今天讲述的那些原则),等老王有空了,将相关代码放到github上,大家可以下载下来copy。

今天,就到这儿吧,有啥问题和想法,欢迎在老王的公众号(simplemain)中提出来~


    关注 SimpleMain


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册