寫(xiě)這篇文章的初衷來(lái)源于最近心血來(lái)潮想獨(dú)立開(kāi)發(fā)一款自己的產(chǎn)品,由于本人一直從事Android開(kāi)發(fā),但產(chǎn)品又需要數(shù)據(jù)的支持,迫于無(wú)奈又撿起了我那半桶水的服務(wù)端開(kāi)發(fā),在做架構(gòu)設(shè)計(jì)的時(shí)候遇到了一些問(wèn)題,下面想談?wù)勗赟ervier/APP通訊中對(duì)用戶身份安全認(rèn)證的一些思考,也希望可以拋磚引玉,和大家一起探討出更好的解決方案。
本文章打算從下面幾個(gè)點(diǎn)切入:
1、認(rèn)識(shí)什么是Session和Token認(rèn)證?
2、Session和Token認(rèn)證分別會(huì)引發(fā)些什么問(wèn)題?
3、如何解決這些問(wèn)題,還有更深層次的思考?
Session認(rèn)證
由于HTTP請(qǐng)求協(xié)議是無(wú)狀態(tài)的,用戶每次訪問(wèn)服務(wù)器,對(duì)服務(wù)器而言都是全新的訪客,服務(wù)器沒(méi)有辦法識(shí)別是哪個(gè)用戶,更不用說(shuō)去記錄用戶上一次的活動(dòng)狀態(tài)了,所以我們需要某種機(jī)制來(lái)維持這個(gè)關(guān)系,就這樣Session誕生了,每一個(gè)Session都對(duì)應(yīng)著一個(gè)SessionId(唯一標(biāo)識(shí)),在這個(gè)Session里可以保存用戶的各種狀態(tài)信息,那服務(wù)器要怎么識(shí)別Session具體屬于哪個(gè)用戶的呢,此時(shí)Cookie就派上用場(chǎng)了,它是存放在客戶端的,里面記錄著一個(gè)SessionId,只要用戶在請(qǐng)求服務(wù)器的時(shí)候,帶上這個(gè)Cookie,也就可以和服務(wù)器“相認(rèn)”了。
Session認(rèn)證會(huì)引發(fā)的一些問(wèn)題
由于Session是存放在服務(wù)器的內(nèi)存中的,隨著用戶量的增加,會(huì)導(dǎo)致服務(wù)器內(nèi)存吃緊,壓力和開(kāi)銷也明顯增大。
當(dāng)業(yè)務(wù)量足夠大,單臺(tái)服務(wù)器吃不消的時(shí)候,我們往往會(huì)做負(fù)載均衡來(lái)分擔(dān)單臺(tái)服務(wù)器的壓力,此時(shí)就會(huì)面臨到一些問(wèn)題,比如用戶在A服務(wù)器登錄建立起Session會(huì)話,當(dāng)再次訪問(wèn)服務(wù)器的時(shí)候,被負(fù)載均衡策略代理到了B服務(wù)器上,此時(shí)B服務(wù)器上是沒(méi)有該用戶的Session會(huì)話的,會(huì)導(dǎo)致用戶需要重新登錄等。
在移動(dòng)端上,每次請(qǐng)求服務(wù)器都要帶上Cookie,有點(diǎn)顯得“過(guò)重”,并且也不是很好“維護(hù)”。
當(dāng)然,以上提到的這些點(diǎn),對(duì)于現(xiàn)在來(lái)說(shuō)已經(jīng)有很多成熟的解決方案了:
比如內(nèi)存吃緊,單臺(tái)服務(wù)器壓力過(guò)大,自動(dòng)擴(kuò)容就可以解決這個(gè)問(wèn)題。
在做服務(wù)器負(fù)載均衡的時(shí)候,可能會(huì)導(dǎo)致用戶丟失Session會(huì)話,只要處理好Session的保持、復(fù)制或者共享即可,比如用Nginx 做負(fù)載均衡的Session保持,Tomcat來(lái)做Session的復(fù)制,通過(guò)KV數(shù)據(jù)庫(kù)來(lái)做Session的共享,比如Redis等。
要在移動(dòng)端上維護(hù)Cookie,GitHub上也有一些開(kāi)源框架,可以很輕松的幫我們處理這個(gè)問(wèn)題。
既然問(wèn)題都可以得到解決,為什么我還想去談Token認(rèn)證,其實(shí)這只是“另外一種”解決問(wèn)題的思路,能讓問(wèn)題更加友好,更加不費(fèi)力的解決。
Token認(rèn)證
Token認(rèn)證也是無(wú)狀態(tài)的,它的出現(xiàn)使得服務(wù)器不再需要為每個(gè)用戶在內(nèi)存中開(kāi)辟一個(gè)Session會(huì)話,簡(jiǎn)單點(diǎn)說(shuō),就是在用戶登錄成功的時(shí)候,服務(wù)器會(huì)給用戶頒發(fā)一個(gè)特殊的令牌(Token),這個(gè)令牌類似于身份證,每個(gè)用戶都是不一樣的,每次用戶訪問(wèn)服務(wù)器的時(shí)候,只需要帶上這個(gè)令牌,服務(wù)器便可知道具體是哪個(gè)用戶,對(duì)于客戶端而言,它只是一串字符串,維護(hù)起來(lái)很輕松,比如可以存放在內(nèi)存,緩存文件,數(shù)據(jù)庫(kù),或者so文件中,對(duì)于服務(wù)端而言,很大程度上節(jié)約了內(nèi)存的開(kāi)銷,只需要將用戶id和token做KV的關(guān)聯(lián)即可。
Token認(rèn)證會(huì)引發(fā)的一些問(wèn)題
1、如何傳遞Token才是比較安全的做法?
2、如何防范當(dāng)傳輸信息被非法盜取后發(fā)起的惡意請(qǐng)求?
針對(duì)以上的問(wèn)題,我們一步步的來(lái)探討:
首先我們對(duì)Token認(rèn)證做了簡(jiǎn)單的介紹,這時(shí)候我們應(yīng)該去考慮,當(dāng)用戶去訪問(wèn)服務(wù)器的時(shí)候,如果只是簡(jiǎn)單的將Token拼接在URL之后,或者附帶在HTTP請(qǐng)求頭里,這樣是很容易被竊取的,由于Token是客戶端和服務(wù)器之間唯一識(shí)別的憑證,只要知道了這個(gè)Token和它所攜帶的方式,那么就可以模擬用戶的一些操作了,比如修改用戶信息,虛擬財(cái)產(chǎn)交易,更改訂單狀態(tài)等。
如何傳遞Token才是比較安全的做法?
1、這里引入一個(gè)比較主流的規(guī)范:JWT
JSON WEB Token(JWT),是一種基于JSON的、用于在網(wǎng)絡(luò)上聲明某種主張的令牌(token)。JWT通常由三部分組成:頭信息(header),消息體(payload)和簽名(signature)。
關(guān)于JWT一些更具體的信息,大家可以在https://jwt.io上了解,這里我就不做過(guò)多的介紹了,我直接切入主題。
JWT它是由三個(gè)部分組成的,分別是頭部(Header),載體(Payload),簽名(Signature),它們之間用字符串“.”進(jìn)行連接,也就是:
頭部(Header).載體(Payload).簽名(Signature)
官方對(duì)每個(gè)部分應(yīng)該包含什么信息也給出了一些建議(并不是強(qiáng)制要求),這里我們來(lái)簡(jiǎn)單看一下:
頭部(Header):
由兩部分組成:令牌類型,加密算法,比如:
{
"alg": "HS256",//加密算法為HS256
"typ": "JWT"http://令牌類型為JWT
}
載體(Payload):
由一些有效信息組成(不應(yīng)該包含敏感信息,比如用戶的手機(jī),密碼等),比如:
{
"sub": "1234567890",//面向的用戶編號(hào)為1234567890
"name": "John Doe",//面向的用戶名為John Doe
"admin": true//是否是管理員
}
到這里,可能有朋友會(huì)問(wèn),這信息也太簡(jiǎn)單了吧,一個(gè)布爾值就可以確定用戶是不是管理員,那么我手動(dòng)修改下信息,那是不是也可以變成管理員了,別著急,我們繼續(xù)往下看。
關(guān)鍵部分,簽名(Signature),它是由頭部和 載體兩部分經(jīng)過(guò)一些編碼處理和加密組成的,官方給了一個(gè)示例:
HS256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
我們可以看出這個(gè)簽名是是由:
經(jīng)過(guò)Base64編碼的頭部 + 字符串“.” + 經(jīng)過(guò)Base64編碼的載體一起拼成后的字符串,再和秘鑰secret進(jìn)行加密算法(頭部指定的HS256算法)組成。

如上圖所示,可以我們就可以得到JWT了,我們?cè)賮?lái)梳理一下流程:
當(dāng)用戶登錄服務(wù)器的時(shí)候,服務(wù)器會(huì)根據(jù)用戶的信息生成JWT,并保存在KV數(shù)據(jù)庫(kù)里,然后回傳給用戶生成的JWT,用戶下一次再訪問(wèn)服務(wù)器的時(shí)候只需要帶上這個(gè)JWT,即可完成身份的驗(yàn)證。

為什么這樣可以保證信息的安全,因?yàn)楹灻⊿ignature)是由服務(wù)器通過(guò)頭部和載體和加密算法組成,雖然頭部和載體是可以通過(guò)Base64解碼成明文的,但是我們?cè)谏珊灻臅r(shí)候已經(jīng)確定好的里面的內(nèi)容,加入用戶篡改頭部或者載體的信息,比如把管理員權(quán)限字段設(shè)置為true,那么和我們服務(wù)器所存的簽名肯定是對(duì)應(yīng)不起來(lái)的,這時(shí)候服務(wù)器就會(huì)拒絕掉該用戶的請(qǐng)求。
關(guān)于JWT的優(yōu)點(diǎn):
由JSON格式組成,所以具有跨平臺(tái)性,可以支持各種編程語(yǔ)言。
減少了服務(wù)器的壓力,相比在內(nèi)存中開(kāi)辟Session,存儲(chǔ)字符串明顯來(lái)的省力許多,但是這里需要注意用戶的規(guī)模,雖然這里我們減少了I/O的開(kāi)銷,但是我們卻多了加解密的過(guò)程,兩者需要權(quán)衡。
解決了分布式共享Session的問(wèn)題,不需要再去考慮文章開(kāi)頭所提到的負(fù)載均衡策略所帶來(lái)的問(wèn)題。
關(guān)于JWT的缺點(diǎn):
沒(méi)有什么東西是完美無(wú)瑕的,JWT雖然解決了一些問(wèn)題,但是也同時(shí)帶來(lái)了一些新的問(wèn)題,我列舉幾個(gè):
無(wú)法控制單點(diǎn)登錄,假設(shè)用戶在A設(shè)備登錄了服務(wù)器,服務(wù)器正常返回JWT,用戶又在B設(shè)備登陸了服務(wù)器,服務(wù)器又返回一個(gè)JWT,此時(shí)2個(gè)JWT都是可用的,服務(wù)器沒(méi)辦法踢出其中任意設(shè)備的用戶。
當(dāng)用戶退出登錄或者修改密碼的時(shí)候,JWT沒(méi)有辦法回收,之前和朋友討論過(guò)也看了一些開(kāi)源項(xiàng)目,大多數(shù)的做法就是直接把JWT丟掉,類似刪Cookie形式,但是這里還是存在安全隱患,如果被竊取,依舊會(huì)造成一些問(wèn)題,因?yàn)檫@個(gè)JWT還是有效可用的。
可能有些朋友會(huì)說(shuō),在載體里面設(shè)置JWT的有效時(shí)間不就好了,但是這樣又會(huì)導(dǎo)致新的問(wèn)題,這個(gè)有效時(shí)間多久合適?太短,會(huì)造成用戶頻繁的過(guò)期登錄,太長(zhǎng),當(dāng)發(fā)生以上問(wèn)題的時(shí)候,還是要忍耐有效期內(nèi)的操作。關(guān)于安全操作,回到文章開(kāi)頭的問(wèn)題,當(dāng)JWT被抓包竊取后,是不是就可以模擬用戶(有效期內(nèi))操作了?比如修改用戶的訂單信息,頻繁的調(diào)用驗(yàn)證碼接口等,這里涉及到URL的重放等攻擊,先不做深究,有時(shí)間再寫(xiě)一篇文章來(lái)詳細(xì)說(shuō)明。
如何防范當(dāng)傳輸信息被非法盜取后發(fā)起的惡意請(qǐng)求?
2、另外一種加密Token的方式
整體執(zhí)行流程還是不變的,只是在處理這個(gè)Token的時(shí)候,讓它變得更安全(加密,時(shí)效,可控)
具體流程:

客戶端在登錄的時(shí)候除了賬號(hào)密碼一些信息外,還要額外的帶上客戶端的本地時(shí)間戳。
服務(wù)器驗(yàn)證賬號(hào)密碼,并求出當(dāng)前服務(wù)器時(shí)間戳減去客戶端時(shí)間戳的時(shí)間差,并生成一個(gè)Token,返回給客戶端的信息為:用戶編號(hào)(不一定是數(shù)據(jù)表的自增id,出自一些安全性的考慮,文章篇幅有限,這里就暫不拓展講了),時(shí)間差,Token等信息,并在KV數(shù)據(jù)庫(kù)(比如Redis)建立用戶編號(hào)->Token關(guān)系。
客戶端存儲(chǔ)用戶編號(hào),Token和時(shí)間差,再下一次訪問(wèn)服務(wù)器的時(shí)候,利用訪問(wèn)的URL,用戶編號(hào),Token,訪問(wèn)時(shí)間(客戶端本地時(shí)間戳 + 時(shí)間差)加密(可以是對(duì)稱或者非對(duì)稱加密)形成一個(gè)簽名(signature),然后再重新拼接url+用戶編號(hào)+訪問(wèn)時(shí)間+簽名 去訪問(wèn)服務(wù)器。
服務(wù)器接收到客戶端請(qǐng)求,首先判斷訪問(wèn)時(shí)間是否超過(guò)當(dāng)前時(shí)間30秒(自定義),如果有則拒絕請(qǐng)求,如果沒(méi)有,根據(jù)用戶編號(hào)到KV數(shù)據(jù)庫(kù)獲取Token,然后進(jìn)行同樣的算法得出簽名與客戶端傳遞的簽名相比對(duì),如果一樣則方可通過(guò),否則拒絕。
這種方案的好處:
讓訪問(wèn)鏈接具備時(shí)效性,為什么要去判斷這個(gè)訪問(wèn)時(shí)間?就是防止鏈接被重用(URL重放),避免接口被抓包獲取,被重復(fù)使用,這樣一來(lái),就算完整的鏈接暴露了,這個(gè)鏈接的有效訪問(wèn)時(shí)間也只有30秒。
解決了用戶單點(diǎn)登錄,修改密碼,登出的問(wèn)題,因?yàn)門(mén)oken是由服務(wù)器維護(hù)的,在用戶信息以上操作的時(shí)候,我們對(duì)Token進(jìn)行刷新即可,利用舊的Token再生成簽名自然校驗(yàn)就不通過(guò)了。
關(guān)于客戶端Token的保存
這里針對(duì)客戶端而外再提一下關(guān)于Token的保存,畢竟客戶端存在被反編譯的風(fēng)險(xiǎn)的,因?yàn)楸救藢?duì)Android開(kāi)發(fā)比較熟悉,這里就拿Android來(lái)舉例:
做好代碼的混淆和加固是最根本的。
不要把這些比較重要的信息寫(xiě)在配置文件,緩存文件,數(shù)據(jù)庫(kù)里等,因?yàn)锳ndroid系統(tǒng)是可以通過(guò)Root拿到高級(jí)權(quán)限的。
我們可以把Token的生成策略存放so文件中,然后用JNI去調(diào)用,因?yàn)閟o文件是由C++編寫(xiě)的,反編譯難度大大增大,可能有人會(huì)說(shuō)那我拷貝so文件,不就依舊可以在其他地方使用了嗎?所以我們也可以采取點(diǎn)其他方法,比如添加包名的驗(yàn)證等。
好了,文章到這里就收尾了,以上是本人對(duì)服務(wù)端身份校驗(yàn)的一些理解與思考,由于篇幅的限制,有些地方?jīng)]有辦法講的太細(xì),也肯定存在不足的地方(比如在登錄,發(fā)送驗(yàn)證碼,下訂單等涉及到安全操作的時(shí)候使用HTTPS),希望有更好方案的朋友一起交流。