转载:http://blog.sina.com.cn/s/blog_80a6423d0102wm74.html
目前,很多网站或app都要求用户用手机注册,比如滴滴打车的注册界面是这样的:
流程大体分两步:
-
用户输入手机号,点击“获取验证码”(滴滴界面上叫“验证”),这时服务器会给用户的手机发送一条短信
-
用户查收短信后,输入短信验证码,点“注册”,服务器进行验证,如果正确,执行注册逻辑
常规的服务器端处理流程
-
第一步,服务器生成一个四位随机码作为短信验证码,发短信出去,同时在数据库或redis里,记录下该手机号对应的这个验证码以及超时时间
-
第二步,用户输入验证码点“注册”后,服务器端在数据库或redis里取到上步记录的验证码,进行对比,如果相同,认证成功,继续后续业务处理
大家可以看到,常规的服务器端处理,是需要操作数据库或redis的,如果数据库或redis挂掉,用户注册这个关键业务就没法进行了。 即使不挂掉,它们也可能成为性能瓶颈
滴滴在《高可用架构》会场上分享了他们的实现方案:把方案做成无状态的,即,不依赖数据库或redis。 这是个很棒的思路,无状态带来的最好处多多(比如方便扩容、、)
在会议现场提问环节,我提出了改进意见,由于时间仓促,没有与主持人深入交流,会后也因为私事匆匆离开,也没机会进一步交流
下面是我的改进方案,不涉及短信防刷这类“无关”问题(因为无论哪种方案,都需要处理防刷)。 本质原理与滴滴的方案相同,全都是在非对称摘要算法上做文章
第一步:用户输入手机号,点击“获取验证码”
这时,http request包含了用户填写的手机号:
phone= 18612345678
服务器端,生成一个随机的四位码作为短信验证码(verify_code),发短信出去(这步略),为了方便,我用ruby代码表达,下同
verify_code = " #{rand( 10 )} #{rand( 10 )} #{rand( 10 )} #{rand( 10 )} "
算出过期时间exp(验证码5分钟后过期):
exp = Time.now + 5.minutes
假设服务器端有一个全局的SECRET(注意别泄漏):
SECRET = "THIS_IS_A_SECRET"
我们把这几项拼成一个大的string,算一个摘要值(token)出来:
require ( 'digest' ) token = Digest::SHA256 .hexdigest(phone + verify_code + exp + SECRET )
摘要算法我这里选了SHA256,可以根据情况调整
在本次请求的http response中,把exp和token传回客户端,类似:
{ " exp ": "2016-07-03 00:32:19 +0800" , " token ":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
}
第二步:用户输入短信验证码,点“登录”
这时,用户提交的http request中包含如下信息(在服务器端,我们用四个变量表示它们 ):
phone= 18612345678
verify_code_input= 用户填的短信验证码
token= 上步传回的值
exp= 上步传回的值
服务器端先检查一下短信验证码是不是超过5分钟有效期了:
if Time .parse(exp) < Time .now halt( "短信验证码已失效,请重新获取" )
end
再检查用户输入的短信验证码是不是正确,算法跟上步一样:
require ( 'digest' ) token2 = Digest::SHA256 .hexdigest(phone + verify_code_input + exp + SECRET )
if token2 != token halt( "短信验证码不正确,请重新获取" )
end
# 下面是验证通过后的代码了...
这样整个验证过程就完成了
附个流程图:
后记:可行性和安全性
攻击者因为手里没有SECRET,所以无法伪造token。 这一点保证了整个方案的可行性
同样因为有SECRET,所以攻击者用rainbow table破解就不可行了,而且有exp定义超时时间,安全性有进一步提升。 实在不小心,SECRET泄漏了,攻击者可以实施的威胁就大的多了,不过这也是其它依赖SECRET的方案的一个通用问题(比如滴滴目前的方案)
服务器端换SECRET,造成的影响不过是最近几分钟内用户获取的短信验证码无效,需要用户再获取一次而已,可以接受