验证码的校验原理其实很简单:
- 客户端请求验证码;
- 服务端生成校验码(code_key-code_value),code_key是一次验证码请求和校验的标识(也有称会话id),会返回给客户端;code_value表示正确的验证码(通常以字符串表示);如果是图片这类的验证码,则需要将图片输出到客户端。
- 用户输入验证码(input_code_value);
- 客户端会将code_key和输入的验证码(input_code_value)发送至服务器进行校验,如果input_code_value=code_value则表示校验通过。
而我们下面要探讨的是,在不同场景下,处理校验码的一些方案;主要包括验证码存在哪里、由谁来校验、怎么校验这几个方面。
1. 单台服务器验证码校验(session)
相信大家都有了解session的原理,客户端访问服务器后,服务端给客户端返回一个叫JSessionId的cookie,服务器可以根据这个值来标识本次会话。单台服务器下,session是一个不错的验证码存储的地方,我们不用担心各个会话间的验证码存在冲突。
以 用户登录+图片验证码+session 为例,其流程主要为:
这里code_key可以理解成JSessionId+session.attribute中的key(这里是"code")
2. 服务器集群下的服务器校验(MySQL/Redis)
当单个服务(如用户中心)使用多台服务器部署后,因为需要做负载均衡,一般情况下(暂不考虑IP-hash那样的策略),用户每次发起的请求有可能会去到不同的服务器,这时候session就会失效了,因为JSessionId这个标志去到别的服务器是找不到对应的session的(事实上,传统的session已经慢慢退出江湖)。
这时候,我们需要自己掌控code_key的生成和存储,关系型数据库(如MySQL)或高性能的NoSQL数据库(如Redis)都是不错的选择。以 用户登录+图片验证码+redis 为例:
可以看到,与单台服务器主要差距在:
- 自己生成code_key(为了防止不同用户间验证码的冲突,生成规则通常与用户标识有关,比如用户id+随机数的组合转换),不再依赖JSessionId;
- 验证码的存储的地方从session转移至redis。
3. 各司其职,分布式下的验证码校验(验证中心)
在上面两种方式,我们发现验证码的生成、存储、校验、业务处理等动作均是在业务方完成的,验证码逻辑和业务的耦合可能会导致一些问题:
- 验证码的风险(比如机器识别难度低、验证接口被攻击等)将由业务方来承担,而业务方往往不愿意投入过多的精力在上面;
- 当我们需要升级验证码方式时(比如图片文字识别 -> 文字点击顺序/拖动/手机验证码等),往往需要服务器做出不小改动和发版,成本较高。
为了解决上面的这些问题,我们可以建立一个专门搞验证的服务——验证中心:
- 验证中心负责各种类型的验证码校验和存储(如采用MySQL/redis,校验方案与集群的那种一致),并提供足够安全的验证码类型供业务方选择;
- 输出验证码(code_key)和第一次校验由客户端和验证中心完成,业务方不参与该阶段验证;
- 验证中心第一次校验成功后生成对应的ticket(比如uuid)给客户端,业务方需要根据code_key和ticket到验证中心进行二次校验,确保验证结果有效。
仍以用户登录为例,我们现在来看看初步的方案:
我们需要明白的是,客户端一共发起三次请求:(1)请求验证中心输出验证码(2)请求验证中心校验验证码(3)请求业务方二次校验并处理业务。
这个初步的方案并不完美,因为在第(3)个请求,也就是二次校验时,只要code_key-ticket相互对应都会通过,这会导致:
- 不同的业务系统间的code_key-ticket混用;本来是业务A系统接口下(比如社区的回贴)所需的图形验证码,经过验证中心首次校验后,产生的code_key-ticket,直接拿到业务B系统接口下(比如支付系统的转账)依然适用(攻击者便可以轻松跳过转账接口的较为严格的首次校验);
- 同一个业务系统,不同接口间的code_key-ticket混用;本来是业务A系统【接口1】下产生的code_key-ticket,拿到A系统的【接口2】上依然适用;
为了解决以上问题,针对每个业务方,验证中心需要分配一个的账号(也就是我们常见的app_id和app_secret);
针对某个业务方下的每个需要验证码的业务接口,需要分配一个的id(姑且叫business_id),它标识了对应的业务和接口,以及使用什么类型的验证码(当然这个类型应该支持动态配置),其关系如下:
加入这些元素后,我们的流程基本不变,只不过多了一些参数校验而已,最终的流程:
也有一些方案是没有business_id这个东西的,他们的business_id等同于app_id,一个业务方中也就只有一个。像我司的验证中心就是一个app_id,客户端、业务方均使用这个。这个主要看大家的细粒度需要分成什么程度了
4.总结
验证码实现方式 | 实现成本 | 优缺点 |
---|---|---|
session | 低 | 只适用于单机;安全性依赖于业务团队水平 |
业务方+MySQL/Redis | 中 | 实现相对简单,能满足大部分使用场景 ;不够灵活,变动成本相对较高;安全性依赖于业务团队水平;各个业务系统需要自己搞一套,总的性价比不高。 |
验证中心+MySQL/Redis | 高 | 给业务方提供足够的安全性以及便利性(客户端/服务端对接验证中心提供插件支持,对接成本低);验证码类型的升级/变更,服务端不需要进行代码变更;前期验证中心的搭建投入成本较高,需要保证足够的tps支持业务需求,但一旦项目成熟,后期回报可观。 |