前面兩節(jié)主要是對redis的基礎知識的梳理商乎,這一篇更多是作者對常見的應用場景的羅列和分析破喻,一共囊括了如下五個應用場景:
- 進行用戶注冊和登錄的全流程
- 保持用戶在線的功能
- 獲取用戶在線的好友
- 通過IP地址查詢訪客所在地址
- 搜索中常見的auto-complete功能
進行用戶注冊和登錄
用戶注冊和登錄的功能,常見的模塊一般有三個:
- 用戶注冊
- 用戶登錄
- 忘記密碼
- 安全防護
用戶注冊
這三個模塊都有各自涉及到的技術點褥赊。首先是用戶注冊模塊袱箱,要滿足用戶注冊的需求,首先明確用戶信息存儲所需要的數(shù)據(jù)結構。一般而言咆槽,一個基本的用戶注冊陈轿,需要如下的字段:
- 用戶名
- 郵箱
- 密碼
- 注冊時間
- 手機等其他需要額外標注的信息
- 可擴展的字段(你不知道之后業(yè)務的形態(tài)發(fā)展,如果你不想連表查詢的話)
那么在這種需求之下,redis中的哈希類型來存儲用戶的基本信息是非常適合的麦射,從存儲優(yōu)勢上面來講蛾娶,使用哈希類型的存儲能夠讓每個用戶的非必填字段不占用多余的內(nèi)存。那么每次創(chuàng)建用戶的時候潜秋,只需要在業(yè)務層保證用戶必填的字段都存在蛔琅,然后插入新用戶的信息即可:
HSET user:$userid userName $userName email $email password $password registerTime $registerTime ...
與此同時,為了保證用戶注冊的唯一性峻呛,我們將用戶的郵箱作為獨一無二的存在罗售,每次新用戶注冊的時候,只需要使用HEXISTS user:$userid email $email
就可以判斷出用戶是否注冊過钩述。這樣做的好處是顯而易見的寨躁,很多熱門的產(chǎn)品,都會進行賬號的搶注牙勘。那么在搶注的過程中,一個高效的,用戶是否已經(jīng)注冊的服務是非常有必要的竞漾。否則一旦過載岩榆,會使得所有的注冊用戶都被拒之門外,這對于產(chǎn)品的傷害性無疑是巨大的恭金。
再者操禀,就像每一個web安全的教程里都會說的,密碼不能被明文存儲蔚叨。這點無論是對mysql還是redis來存儲密碼都是毫無疑問的床蜘。常見的方式是對密碼使用不可逆的單向加密算法,比如Md5算法(應該說是一種摘要的算法)蔑水,同時在生成最終存儲的密碼時還需要一個由服務端控制的對應每個用戶的鹽來避免可能的Rainbow table的攻擊邢锯。這個鹽一般會寫在服務端的代碼配置或是在用戶表中增加一個字段進行單獨的存儲。但是對這個鹽的維護成本較高搀别, 而且沒有必要丹擎。
所以更好的方式其實是使用bcrypt算法,他主要有幾個不錯的優(yōu)勢:
- 它的算法是漸進式的歇父,會隨著你嘗試的次數(shù)增加而增加計算的時間蒂培,也就是說如果有人用brute-force攻擊的話,那么他就必須具備大量的資源榜苫,也就是大量的肉雞來嘗試护戳;
- 它使用了blowfish算法來對密碼進行哈希,這樣做的優(yōu)勢在于加密的每一步都完全取決于salt和用戶的密碼垂睬,攻擊者很難在沒有兩者的情況下進行模擬媳荒;
- 它是單向的加密算法抗悍;
- 可配置的round參數(shù),round為5的情況下钳枕,一個6位的密碼破解需要數(shù)年的時間缴渊;
- 無需存儲salt,算法本身會把salt加入到最終生成的值中鱼炒,這就省去了對salt的單獨處理
關于bcrypt衔沼,有幾篇不錯的post推薦:
- bcrypt中的鹽:https://stackoverflow.com/questions/277044/do-i-need-to-store-the-salt-with-bcrypt
- 加密中的鹽和胡椒:https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords/16896216#16896216
- 哈希md5和加密crypt有啥不同:https://stackoverflow.com/questions/4948322/fundamental-difference-between-hashing-and-encryption-algorithms/4948393#4948393
- php如何使用bcrypt:https://stackoverflow.com/questions/4795385/how-do-you-use-bcrypt-for-hashing-passwords-in-php
由此,我們就解決了用戶注冊中最大頭的用戶信息存儲昔瞧、用戶注冊校驗指蚁,以及用戶密碼加密的三個部分。接下來繼續(xù)進入到用戶登錄與忘記密碼模塊硬爆。
用戶登錄與忘記密碼
對于每個用戶欣舵,我們需要用用戶id進行關聯(lián),這個用戶id同時也可能關聯(lián)了其他的業(yè)務相關的存儲缀磕。這些存儲并不一定是redis或是mysql缘圈,但是必須要有這么一個表示保證業(yè)務的連通性。所以我們單獨保存了郵箱與用戶id的映射關系袜蚕。而每當用戶登陸的時候糟把,我們就可以:
- 通過郵箱,根據(jù)映射獲取用戶的id
- 與redis中存儲的哈希數(shù)據(jù)對比郵箱和密碼
如果驗證通過牲剃,那么用戶登錄成功遣疯,通過相應的方式保持用戶的在線狀態(tài)。這里簡略的說兩種方式凿傅,后文會詳述:
- 瀏覽器cookie存儲缠犀,通過cookie保存用戶的登錄標識
- 后端保持登錄,服務器session保持用戶的登錄狀態(tài)
另外一個常見的場景就是與郵件相關的聪舒,比如忘記密碼辨液,比如校驗郵箱的有效性。這些功能都需要異步的發(fā)送郵件箱残,同時在用戶進行相應的鏈接操作之后滔迈,修改特定的狀態(tài)。我們以修改密碼為例被辑,一旦用戶發(fā)出了修改密碼的請求燎悍,那么需要做如下幾件事情:
- 郵箱有效,發(fā)送修改密碼的郵件盼理;
- 在發(fā)送的郵件中的修改密碼的鏈接中谈山,生成隨機的驗證碼,并且設置固定的有效期為一個小時(expire命令)宏怔;
- 提供校驗驗證碼的服務奏路,如果用戶在一個小時之內(nèi)點擊跳轉抗蠢,并且驗證碼校驗通過,提供修改密碼的頁面思劳;
與此同時需要注意的是,必須對用戶此類涉及到郵箱的服務妨猩,進行訪問頻率限制潜叛,從而防止大量無效的郵箱發(fā)送請求。
用戶在線保持
用戶在線保持的方案有很多壶硅,上文中也提到了威兜,主要是分成兩個流派。
第一個流派就是使用前端的存儲來記錄用戶的狀態(tài)庐椒,包括但不限于cookie椒舵、端的本地存儲等等。每次用戶請求的時候约谈,都會帶上這個客戶端的標識笔宿,而服務端提供相應的校驗機制。這么做的好處是服務端實現(xiàn)比較簡單棱诱,只需要做兩件事:
- 在用戶登錄的時候泼橘,種上有一定時間期限的標識
- 用戶請求的時候,對于此類標識進行校驗
但是不可避免的迈勋,使用客戶端存儲用戶標識會有一定的安全風險炬灭,其中的風險在于:
- cookie可能會被xss攻擊獲得,從而破解了用戶的登錄身份
- 一些不應該暴露給用戶的信息靡菇,通過本地存儲的方式暴露給用戶了重归,存在潛在的安全風險。
因此厦凤,第二個流派就是服務端的登錄態(tài)保持鼻吮。這個登錄態(tài)保持由來已久,之前最經(jīng)典就是服務端的session泳唠。其依賴于一個sessionid(一般也是存儲在cookie中的)作為key狈网,然后將用戶的登錄信息存儲在服務端。存儲在服務端的方式有很多笨腥,redis作為服務端內(nèi)存存儲的佼佼者拓哺,當然是一個不錯的方式。
首先我們使用一個散列來存儲sessionid與已登錄用戶之間的映射脖母。要檢查一個用戶是否登錄士鸥,需要根據(jù)給定的sessionid來查找與之對應的用戶,并在他已經(jīng)登錄的情況下谆级,返回用戶的ID烤礁。
hget login: $sessionid
而比較復雜的情況出現(xiàn)在用戶的登錄態(tài)刷新上讼积,每當用戶瀏覽頁面的時候,我們都對用戶存儲在登錄散列里面的信息進行更新脚仔,并將用戶的sessionid和當前時間戳添加到記錄最近登錄用戶的有序集合中勤众。
hset login: $sessionid $user
zadd recent: $sessionid time()
一般而言,單臺redis的存儲容量有限鲤脏,并且為了不占用不必要的內(nèi)存们颜,我們肯定需要對用戶的登錄狀態(tài)進行清理。假定產(chǎn)品設計只需要同時保持100萬個用戶在線猎醇,那么我們可以通過定時腳本的方式窥突,在存儲超限的情況下,找到最舊的sessionid硫嘶,將其清理阻问。
while
size = redis.zcard('recent:')
if size <= LIMIT
time.sleep(1)
continue
end_index = min(size - LIMIT ,100 ) //每次不要刪除太多
sessionids = redis.zrange('recent:',0,end_index-1)
for sessionid in sessionids:
session_key = 'delete:'+sessionid
multi
delete session_key
hdel login: session_key
zrem recent: session_key
exec
在實際運行時,每秒最多可以清理60000多個令牌沦疾,完全夠用了称近。
本文主要對redis在登錄注冊、登錄態(tài)保持方面的應用進行了探討曹鸠,同時也結合了一些我的實際使用與開發(fā)的經(jīng)驗煌茬。有人說redis的使用非黑即白,需要摒棄關系型數(shù)據(jù)庫彻桃。但是在實際的工程中坛善,頗為不可行,很多系統(tǒng)仍然依賴于關系型數(shù)據(jù)庫存儲的方方面面邻眷,這點在后文中也會有所體現(xiàn)眠屎。接下來的一文,會對用戶的好友列表拉取肆饶、auto-complete實現(xiàn)改衩、以及ip地址查詢訪客等功能進行更進一步的討論。