轉載: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,造成的影響不過是最近幾分鍾內用戶獲取的短信驗證碼無效,需要用戶再獲取一次而已,可以接受