之前一直认为,验证码是 web 应用中,体验最糟糕的环节,登录或注册账号时,最烦去识别那些验证码。 所以在开发产品的时候,一直没把验证码列入开发计划。虽然知道有隐患,但觉得我们增速不会太快,没必要那么早注重安全环节。
但是,最近审查产品的请求日志,发现有异样:
有很多请求,没有入口页面,上来就请求指定接口,并且请求 HEADER 信息一致。懂 web 的人肯定能看出,写个脚本,实现这种请求,非常容易。
在去年,抢火车票,抢小米,使得国内爆破 web 接口的技术水平,迈上新台阶。抢东西倒还好,碰撞密码才是大危害。 如果没验证码,那等于是虚掩着大门,开发者可以轻松推开;加了验证码,至少还有把锁,想进去的人,需要一些技巧,才能开。
这周在研究 CAPTCHA 实现方案时,发现这是 Ruby 的短板之一。 开源的方案中,大都是依赖 ImageMagick 这种第三方库去实现,验证码的图片很小,并不复杂,用个那么庞大的库,太浪费。
后面考虑用其它语言,包括 Java、C#,这类语言生成个验证码图片,很轻松,我们团队也都熟悉语法,但是运行时太庞大。 最后,考虑到 Golang,它的标准库带了图像处理,而且安装非常方便,解压下就能用,虽然不熟语法,但有静态语言经验,学起来不难,而且 Github 上有相关代码可供参考:https://github.com/dchest/captcha。
在实现图片验证码前,仔细看过乌云的两篇文章:初探验证码识别,常见验证码的弱点与验证码识别 。
不得不承认,国内的智能破解验证码水平,已经很高。所以在生成验证码图片时,不要太天真。 我们用的策略,就是尽可能随机,包括:背景色和字体色,字体倾斜角度,验证码位数等。
为了不影响常规体验,我们也是在登录失败达到一定次数后,强制用户输入验证码,这个次数统计,我们是放在 Redis 里面做,全局的。有些产品是把这个计数器,存在 Cookie 里,Cookie 是客户端传来的,不可信。
我们已经将这套方案上线:
简单说下我们的实现: 1) 提供独立的域名//captcha.ibanquan.com,来呈现验证码,这个域名的后端是 Golang 进程; 2) Ruby 中通过 Redis 记录一个账号的登录失败次数,一旦超过指定值,那么登录时必须带合法验证码; 3) 需要用到验证码时,前端发起请求://captcha.ibanquan.com/?callback=funcname(这里用 JSONP 避免跨域问题)返回:
func && func({"id":"8a362d95a1d5289b3429be8b936c9d31",
"path": "/image/8a362d95a1d5289b3429be8b936c9d31.png" });
4) 前端把上面返回的图片路径,放到域名后面: //captcha.ibanquan.com/image/8a362d95a1d5289b3429be8b936c9d31.png 作为标签的 src 就可以呈现验证码图片,再把 id 的值,隐藏在表单中; 5) 表单提交时,把用户输入的验证码,以及隐藏的验证码 ID 提交给 Ruby 后端; 6) Ruby 里面,通过验证码的 ID 从 Redis 取出对应 Value,和用户输入的验证码匹配。
随手点开社区的酷站,发现很多产品,要么没 CAPTCHA 机制,要么图片验证码太简单。 尤其是我们社区,都还没有 CAPTCHA 机制。其实开发者社区账号,更有价值,一旦开发者账号攻陷后,域名的账号、主机账号等都可以去碰撞,一旦得手,可以为所欲为。
最后,推荐一位国内安全领域牛人,微博 ringzero,乌云里核心白帽子猪猪侠。