概述
在很多的Web应用系统当中,图形验证码即"CAPTCHA"是一个比较常见的增强系统安全的机制。它来自一个英文语句“Completely Automated Public Turing test to tell Computers and Humans Apart”的主要词汇的起始字母缩写,原意为“用于区分计算机和人类的完全自动公开图灵测试”, 但这个名称显然过于学术化, 我们一般就简单的把它称为验证码。
它的最常见的场景是,在用户登录的时候,除了基本的用户名和密码等标识和验证信息之外,需要额外的输入一个验证码。这个验证码的内容通常来自一个登录页面中同时展示的图片。这样设计的原因是要防止某些程序的自动化登录,从而保护认证过程,提高系统的安全性。在这种情况下,这个机制的有效性,建立在一个假设前提之下,就是对于图片中展示的内容(通常是一些文字字符),人类可以比较容易的识别,但自动登录程序由于不能识别其中的文字,而无法继续登录过程。
当然,基于图形中文字识别,只是CAPTCHA机制的一种比较常见和广泛的实现方式,其他还有很多如按顺序点击图片区域,拖动鼠标对齐,回答逻辑或者计算问题等等,虽然形式和实现方式不同,但它们的目的都是相同的。本质上就是一次图灵测试,来区分计算机系统和人类。
关于这个机制的底层逻辑和原理和其他的实现方式,以及这个图形验证码本身的生成实现方式和安全性的问题,笔者不想在本文中深入探讨。本文想要研究的问题是,基于现有常用的图形验证码,如何更好的完成应用和验证的过程。
在继续之前,笔者想以正在维护的一个比较老的应用系统为例,来说明一下captcha的传统实现和应用模式。
传统应用方式和问题
笔者的应用系统中,提供了一个captcha接口,请求这个接口,客户端可以获取一个图片文件流,并可以将这个图片展示在页面当中让用户在登录时,识别并输入其中的文字。
其实在输出图片之前,相关的captcha模块,还做了一些工作。包括先随机产生一个字符串,然后以这个字符串为输入参数,调用一个图形生成程序模块,来生成一个带有这些字符的图片。当然为了提高安全性,这个图片生成程序,会将输出的文字进行如扭曲、错排、旋转、加噪等处理,让图片更难为程序来识别。
然后,将这个图片,作为一种图片文件流的形式,响应输出给客户端来显示。于此同时,服务端程序,会将这个字符串,记录在session当中。在笔者的系统中,具体而言,是使用redis和某个session key来保存这个文本内容。
当用户登录时,输入验证码并提交请求时,这个验证码信息也会提交到登录验证模块中进行处理。程序会基于session中的key,从redis中取出前面保存的验证码原文内容,和用户提交的内容进行比较,来确认这个验证码是否正确。
以上,就是当前CAPTCHA应用的一般实现和应用过程。显然,这一过程和机制存在一些问题:
- 需要实现和维护一个HTTP session
- 需要使用redis来存储和读取当前的验证码内容
- 验证码的传输并没有加密和编码,安全性不高
- 整个流程环节和操作比较多,影响可靠性、稳定性和性能
- 所有操作都在服务端处理,对服务端程序性能压力较大
由此,笔者在本文中提出了一种基于密码学计算的应用和验证过程,可以改善和解决上面的问题,大大提高CAPTCHA认证过程的体验、安全性和性能。这一方案的核心概念包括:
- 使用密码学机制,可以在客户端进行验证,错误的输入验证可以立即完成,无需网络请求,提高用户体验
- 在服务端进行二次验证,来防止程序攻击
- 无需外部信息存储(如redis),简化系统架构和部署
- 验证过程简洁高效,保证性能
- 在服务端不保存任何验证相关的文本信息,提升安全性
下面是具体的实现和主要的操作过程。
实现和主要过程
在使用和实现方面的过程主要如下。主要包括两次请求和响应过程。
客户端请求验证码
客户端需要先调用服务器提供的一个验证码接口,来获得验证码和验证信息。这次请求需要提供的一个随机信息,我们记为“clientKey”,它用于随机化验证码生成和检查的过程。
clientKey = randomText(8)
在请求提交之后,客户端应记录clientKey,用于后续验证处理。
服务端响应验证码和验证信息
服务端接收客户端请求后,按照普通的方式,生成一对验证码原文和验证码图片。验证码图片可以使用base64编码成为文本,封装到响应体中。
capText = randomText(4~6);
capImage = genCaptcha(capText); // base64编码
capTime = Date.now() + 10min; // 有效时间
接着,需要对这个验证码进行编码,生成两个验证信息,分别用于客户端验证和服务端验证。客户端验证的信息,我们记为"sign1"。服务端验证的信息,我们记为"sign3",其生成方式为:
sign1 = hmac(capText, clientKey);
sign2 = hmac(capText, "FIXKEY");
sign3 = hmac(capTime + sign2, serverKey);
// 响应内容示例:
{ capImage, capTime, sign1, sign3 }
最后,我们将capImage,capTime,sign1和sign3作为响应内容,发送给客户端。注意这里sign2是中间计算结果,是不提供给客户端的(但实际上客户端可以基于输入的信息进行计算)。
另外,由于需要传输附加的验证信息,服务端就不能做成简单的文件流响应接口,而是需要将图片内容封装到响应体中。需要客户端也做相应的改造,可以将图片内容分离并且展示在页面中。
客户端处理响应
客户端在收到服务端响应之后,从响应内容中分离出capImage,展示在页面中,用户在登录时识别使用。同时记录capTime,sign1和sign3用作后续处理。
客户端验证
captcha验证码,通常和登录验证一起使用。人类用户在登录时,需要同步提交在网页上captcha图片识别的文本内容。这是在浏览器中,可以先进行客户端验证。我们假设用户识别并输入的验证码内容是capTextC,则基于已记录的信息,验证方式为:
sign1 ?== hmac(capTextC, clientKey)。
即比较记录的sign1和计算得到的信息,来确定用户是否输入正确。如果验证不通过,程序显示“验证码错误”的提示内容。注意在这种情况下,比较安全的处理方式是再次请求获取新的captcha图片和验证信息。
如果验证通过,则客户端可以正常的提交登录信息来继续登录过程。但这里需要提供一些额外的信息,来帮助服务端进行captcha验证。这些信息包括:
sign2c = hmac(capTextC, "FIXKEY");
sign3c = sign3
// 请求的内容应当包括
{ sign2c, sign3, capTime }
服务端验证
服务端在处理登录请求时,同时收到的captcha验证消息,包括sign2c和sign3c。它可以通过计算(无需记录sign3),来确定这个信息是否是正确的:
capTime ?> now();
sign3c ?==? hmac(capTime+sign2C, serverKey);
服务器首先检查提交的时间是否已经过期。然后基于提交的验证信息和时间,来计算和服务器信息验证信息是否匹配。
由于sign2C只能基于输入的信息计算得来,同时客户端没有服务端密钥,所以客户端无法构造正确的sign3C。所以这一机制可以确保只能由服务端来进行验证和确认。
如果验证不通过,则服务端应该响应“验证码过期或者错误”的信息。否则就可以继续后续的登录信息验证的过程。CAPTCHA相关的应用和流程到这里就已经完成了。
相关算法
文中所展示的示例代码都是伪代码。但它们都比较简单,代表一些常用的标准算法,包括:
- randomText() 可以生成一定长度的随机的字符串
- hmac 标准信息认证码算法,即密钥摘要,注意这里使用了几种不同的密钥
- now() 获取当前时间戳
扩展应用
虽然本文是以图形验证码为例,讨论的验证码的优化应用方式和流程。但实际上,这个方法是通用的,可以应用在各种类似的场景当中。比如手机验证码,其底层逻辑也是一样的,就是假设人类可以简单操作,而攻击者或者程序却不能。而且理论上更加安全,因为它使用的是不同的通信链路,来达到类似的效果。
所以,如果使用短信验证码,或者微信验证码,这个应用方式和流程可以改进如下:
- 客户端请求发送验证码消息(短信或者微信)
- 服务端生成验证码,并发送给消息服务
- 服务端计算验证码签名,包括客户端签名和服务端签名
- 服务端响应给客户端
- 用户收到消息,在客户端界面中,手动输入验证码原文
- 客户端进行验证,可以立即判断此验证码原文是否正确
- 如果客户端验证通过,则构造请求内容,提交给服务端
- 服务端使用请求内容,验证服务端签名
- 验证通过,继续下一步业务流程...
小结
本文分析了当前使用的CAPTCHA验证实现和过程的可能存在的一些问题,并提出了相应的技术和实现改进方案,包括详细的操作流程和参考伪代码。从而能够改善原有方案在安全、性能、可靠性、可移植性和复杂性等方面的问题。