序
验证码,只要一点简单的逻辑,就能避免泱泱脚本大军的骚扰。但利剑往往是双刃的,并不是每个场景都适用,本文将通过记录一起线上事故,来展示使用图形验证码的代价,并讨论如何应对类似情况。
科普一个冷知识:验证码的英文是CAPTCHA,是Completely Automated Public Turing test to tell Computers and Humans Apart的缩写,翻译过来是“全自动区分计算机和人类的图灵测试”(不是“雅木茶”)。
事故过程
一切都要从一个简单的需求说起
公司新游戏即将上线,运营已经激动地搓手手了,于是一次预约活动规划了下来。就是那种输入手机号和手机系统的预约活动。这个需求太简单了,开发也没多想,一梭子代码就下去了。唯一一层防护是为了避免脚本刷接口,要求预约时要输入一次图形验证码。
悲剧即将上演
倒计时3,2,1,活动开始。开始的时候一切顺利,流量流入,预约数据也在有条不紊地入库。不过一会儿,客服就开始忙活了起来,原来大量玩家反馈:本该显示图形验证码的地方现在正显示着一个x。于是,一句”你们公司用的是土豆服务器吗“刷遍了微博贴吧。
开始思考
这是图形验证码的一般做法
在img标签的src填入生成验证码地址,该接口会在内存中生成一张图片。严谨一些的话,还会在图片上面打上干扰线和噪点。
将验证码存入Session后,返回图片。
用户提交数据,比较参数和Session值。
天底下没有理所当然的事
这本来是最正常不过的操作,但仔细过一遍可以发现,生成一张图形验证码的代价是很大的。
生成图片,占用比普通操作更多的内存
生成随机数、噪点、干扰线,需要生成随机数
图片传输,占用带宽
验证码读取\写入Session,需要读写磁盘
实践一下
代码
写一个简单的验证码生成方法:
$str = "23456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVW";
$len = strlen($str) - 1;
$code = '';
for ($i = 0; $i < 4; $i++) {
$code .= $str[mt_rand(0, $len)];
}
$img = imagecreatetruecolor(100, 30);
imagefilledrectangle($img, 0, 0, 100, 30, imagecolorallocate($img, 255, 255, 255));
imagefttext($img, 20, mt_rand(-5, 5), 10, 25, imagecolorallocate($img, 0, 0, 0), '{your font path}', $code);
$_SESSION['captcha'] = $code;
header("Cache-Control: no-cache");
header("Content-type: image/png;charset=utf-8");
imagepng($img);
imagedestroy($img);
没有使用框架、组件,简化加载过程。功能越强大,性能越受限。
为了减少随机数生成,验证码仅四位,背景色、字体色直接固定,也没有加噪点,加干扰线。
生成并输出图片后,销毁资源。
不保存图片到磁盘。
写一个方法,模拟用户看到验证码,输入验证码的过程:
$input = $_POST['verify'] ?? '';
if ($input === '') {
return [
'code' => 0,
'msg' => 'verify empty',
];
}
$code = $_SESSION['captcha'] ?? '';
if ($code === '') {
return [
'code' => 0,
'msg' => 'verify not exist',
];
}
if (0 !== strcasecmp($input, $code)) {
return [
'code' => 0,
'msg' => 'verify failed',
];
}
unset($_SERVER['captcha']);
return [
'code' => 1,
'msg' => '',
];
单次调用
获取验证码用了184ms。来看看机器指标,恩,没什么波澜,系统1分钟平均负载是0.3。
什么是系统1分钟平均负载?
1分钟内,占用全部CPU算力的比例。
举个栗子,如果机器是2核的,那么满负载时,最大值就是2。上面的负载是0.3,意味着程序只用到了1个CPU约三分之一的算力。
这次来试试1000次
go-stress-testing-linux -c 1000 -n 1 -u {your url}
总耗时5s,全部成功(HTTP状态码200),再来看看指标,系统1分钟平均负载上升至1,还好还好。
康康你的极限在哪里?
go-stress-testing-linux -c 10000 -n 1 -u {your url}
总耗时36s,成功5007,失败4993(HTTP状态码509),看看指标:
硬件资源并没有消耗完呀,怎么会有失败请求呢?
原因很简单,上面已经告诉你啦,带宽占满啦。代码生成的验证码图片一张约1.7k,比起其他类型的数据已经大很多了。所谓聚少成多,聚沙成塔,一个人的力量可能不算什么,但千万个人的力量就绝对不可忽视。
各个HTTP状态码表示详见(http://zhaomaomao.net/article/1/13)。
得想个办法让所有请求都进去
要达到这个目的,需要一点点改造:
把生成验证码的逻辑从controller移到Laravel的Command模块,这样,就不会出现php的脚本超时啦。
写一个sh脚本启动这些CMD,这样,就绕过了web服务器。
#!/bin/bash
start_at=`date +%s`
for((i=1;i<=5000;i++));
do
php artisan {your command} > /dev/null;
done
end_at=`date +%s`
echo $[end_at-$start_at]
总耗时1219s,看看指标:
处理5k个验证码生成,已经要花20多分钟了,单从这点看已经不能拿上正式服了。由于没有经过Nginx处理,内存倒没有飙高。
分析一波
由上面几组数据可以看出,自己生成图形验证码消耗资源从多到少依次为:网络带宽 > CPU > 内存 > 磁盘读写。
笔者的虚拟机是2核,4G,3M带宽,在Nginx各项超时时间5分钟的情况下(这个时间已经很长了),每秒也只能正常处理约140个验证码请求。
怎么优化呢
有人可能会问,蛤?这有什么可优化的?就像上面说的, 天底下真的没有理所当然的事情。笔者在此抛砖引玉,献个丑啦。
限制前端的刷新频率
笔者自己就是这样,当看不清验证码心烦意乱的时候,就会疯狂点击刷新。验证码生成本身就需要一定时间,结果刷出新的还没来得及显示,就又进入了下一次生成的循环。
用js稍稍控制一下,在img图片load完成前禁止刷新;刷新后稍稍等个几秒,就可以避免这类情况。
把验证码生成的服务与主要逻辑服务分离
这和静态资源与网站其他资源分离是一个道理。既然验证码图片占带宽,那就不走主要逻辑服务器的流量,这样就可以增大主要逻辑服务器的吞吐量。
使用没有图片的验证码
例如把方块拖动到最右边啦,完成拼图啦(这种也需要图片,但只需要加载一次),把图片旋转到正面啦之类的。
使用第三方验证码
能用钱解决的,都不是问题。
就到这里吧
希望本文或多或少对你有些许帮助。谢谢你的阅读。再见。