憑證管理揭秘:Cookie-Session 與 JWT 方案的對(duì)決
概述
在上一篇文章我們聊完了授權(quán)的過(guò)程,在服務(wù)器對(duì)客戶端完成授權(quán)之后,服務(wù)器會(huì)給客戶端頒發(fā)對(duì)應(yīng)的憑證,客戶端持有該憑證訪問(wèn)服務(wù)端,服務(wù)器便能知道你是誰(shuí),你有什么權(quán)限等信息。這一章我們具體聊聊常見(jiàn)的憑證管理技術(shù)有哪些。
在軟件架構(gòu)中,關(guān)于憑證如何存儲(chǔ)和傳遞,一直有兩種不同的解決思路,兩種不同的解決方式,實(shí)際上反映了兩種不同的架構(gòu)思路:
- 一種是把所有狀態(tài)信息都放在服務(wù)器端 (Cookie-Session 方案)
- 一種是把所有狀態(tài)將信息存儲(chǔ)在客戶端(JWT 方案)
在互聯(lián)網(wǎng)早期,這個(gè)問(wèn)題早就有了明確的答案。大多數(shù)應(yīng)用都采用了 “Cookie-Session” 的方法,這種方法通過(guò)在服務(wù)器上存儲(chǔ)用戶狀態(tài),來(lái)實(shí)現(xiàn)用戶身份的識(shí)別和信息的傳遞。這種方法在很長(zhǎng)一段時(shí)間里都是主流。
然而,隨著微服務(wù)和分布式系統(tǒng)的興起,我們發(fā)現(xiàn)由于 CAP 的限制,服務(wù)器端存儲(chǔ)狀態(tài)信息的方式開(kāi)始面臨很多問(wèn)題(微服務(wù)要求服務(wù)端本身是無(wú)狀態(tài),才能實(shí)現(xiàn)動(dòng)態(tài)擴(kuò)縮容)。這就迫使我們重新考慮被放棄的客戶端狀態(tài)存儲(chǔ)方法。在這個(gè)背景下,JWT(JSON Web Token)的令牌的方案開(kāi)始受到關(guān)注。JWT 是一種在客戶端存儲(chǔ)用戶狀態(tài)信息的方式,它允許用戶在不同的服務(wù)器之間自由切換,而不需要重新登錄。這種特性在分布式系統(tǒng)中非常有用。但是要明白,JWT 和 Cookie-Session 只是對(duì)授權(quán)信息存儲(chǔ)的主體(客戶端,服務(wù)端)不同,各有優(yōu)勢(shì),合適場(chǎng)景不同,不存在誰(shuí)比誰(shuí)要先進(jìn)的問(wèn)題。在本節(jié)中,我們將探討 Cookie-Session 和 JWT 兩種方案的相同點(diǎn)和不同點(diǎn),幫你更好地理解這兩種方案的優(yōu)缺點(diǎn),以及它們?cè)诓煌瑘?chǎng)景下的應(yīng)用。
cookie-session
總所周知,因?yàn)?HTTP 是無(wú)狀態(tài)協(xié)議,所以 Cookie-Session 的原理其實(shí)很簡(jiǎn)單,就是解決 HTTP 協(xié)議無(wú)狀態(tài)的問(wèn)題,在 RFC 6265 中定義了 HTTP 的狀態(tài)管理機(jī)制,增加 Set-Cookie 指令,服務(wù)端向客戶端發(fā)送一組信息(標(biāo)識(shí))示例:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: session_token=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Path=/
客戶端收到指令后在此后一段時(shí)間的 HTTP 請(qǐng)求中都發(fā)給服務(wù)端會(huì)話信息(在 Header 中攜帶 cookie 信息),以便服務(wù)器區(qū)分不同的客戶端:
GET /profile HTTP/1.1
Host: www.example.com
Cookie: sessionid=xyzasdzxc123789456
客戶端的 Cookies 里通常只存儲(chǔ)一個(gè)無(wú)意義,不重復(fù)的字符串,通常命名是 sessionid 或 jessionid ,服務(wù)端則根據(jù)該字符串作為 Key,和用戶信息建立關(guān)聯(lián)后存儲(chǔ)在服務(wù)端的內(nèi)存或者緩存中。再輔以一些超時(shí)自動(dòng)清理的措施來(lái)管理會(huì)話。
它們的交互過(guò)程如下:
圖片
這種服務(wù)端的狀態(tài)管理機(jī)制就是 Session,Cookie-Session 也是最傳統(tǒng),但今天依然廣泛應(yīng)用于大量系統(tǒng)中的,由服務(wù)端與客戶端聯(lián)動(dòng)來(lái)完成的狀態(tài)管理機(jī)制。
總結(jié)
cookie-session 的方案在存儲(chǔ)授權(quán)信息具有以下優(yōu)勢(shì):
- 安全性:由于狀態(tài)信息都存儲(chǔ)在服務(wù)端,cookie-session 方案在安全性上有天然的優(yōu)勢(shì),能完全規(guī)避上下文信息在傳輸過(guò)程中被篡改的風(fēng)險(xiǎn)
- 靈活性:由于存儲(chǔ)在服務(wù)端,服務(wù)端可以存儲(chǔ)各種數(shù)據(jù)對(duì)象,而不僅僅是字符串
- 狀態(tài)控制:服務(wù)端維護(hù)狀態(tài)的優(yōu)勢(shì)在于可以根據(jù)自己的意愿隨時(shí)修改,清除上下文信息,可以很輕松實(shí)現(xiàn)用戶強(qiáng)制下線的功能。
Cookie-Session 在單體服務(wù)環(huán)境中是最合適的方案,但是因?yàn)榉?wù)端有狀態(tài),當(dāng)需要水平擴(kuò)展服務(wù)能力,要部署集群時(shí)就開(kāi)始面臨麻煩了。接下來(lái)的 JWT 令牌就是 Cookie-Session 在分布式環(huán)境的替代品,但是不能說(shuō) JWT 要比 Cookie-Session 更加先進(jìn),更不可能全面取代 Cookie-Session 機(jī)制。
JWT
當(dāng)服務(wù)端有多臺(tái),并且不能存儲(chǔ)狀態(tài)的時(shí)候,客戶端就要承擔(dān)存儲(chǔ)有狀態(tài)(授權(quán)信息)的職責(zé)了。這就是 JWT 令牌的方案思路。
JWT(JSON Web Token)是一種定義在 RFC 7519 標(biāo)準(zhǔn)中的令牌格式,主要應(yīng)用于現(xiàn)代分布式應(yīng)用系統(tǒng)中,經(jīng)常與 OAuth2 協(xié)議配合使用。在深入探討 JWT 的結(jié)構(gòu)之前,我們先來(lái)直觀地了解一下它的基本形式。示例:
圖片
注意:JWT 令牌不加密,只使用 Base64URL 轉(zhuǎn)碼,所以 JWT 令牌里別放敏感信息,令牌只解決防篡改的問(wèn)題,并不解決防泄漏的問(wèn)題,JWT 令牌都可以在 JWT 官網(wǎng)(https://jwt.io)上進(jìn)行解碼。如圖所示,JWT(JSON Web Token)由三部分組成:Header(頭部)、Payload(負(fù)載)、Signature(簽名)。這三部分之間使用點(diǎn)(.)分隔。
Header:Header 部分通常包含令牌的類型(通常是 JWT)和使用的加密算法,例如 HS256:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:Payload 部分包含所需的聲明,這些聲明可以包括用戶信息或其他相關(guān)數(shù)據(jù)。例如,用戶ID和過(guò)期時(shí)間:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
負(fù)載部分,JWT 在 RFC 7519 中推薦(非強(qiáng)制約束)了七項(xiàng)聲明名稱(Claim Name),如有需要用到這些內(nèi)容,建議字段名與官方的保持一致:
- iss(Issuer):簽發(fā)人。
- exp(Expiration Time):令牌過(guò)期時(shí)間。
- sub(Subject):主題。
- aud (Audience):令牌受眾。
- nbf (Not Before):令牌生效時(shí)間。
- iat (Issued At):令牌簽發(fā)時(shí)間。
- jti (JWT ID):令牌編號(hào)。
此外在 RFC 8225、RFC 8417、RFC 8485 等規(guī)范文檔,以及 OpenID 等協(xié)議中,都定義有約定好公有含義的名稱,可以參考 IANA JSON Web Token Registry。
Signature:Signature 是使用 Header 中指定的算法和一個(gè)密鑰對(duì) Header 和 Payload 進(jìn)行簽名得到的。對(duì)前面兩部分內(nèi)容進(jìn)行加密計(jì)算,以例子里使用的 JWT 默認(rèn)的 HMAC SHA256 算法為例,將通過(guò)以下公式產(chǎn)生簽名值:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)
簽名的意義在于確保負(fù)載中的信息是可信的、沒(méi)有被篡改的,也沒(méi)有在傳輸過(guò)程中丟失任何信息。因?yàn)楸缓灻膬?nèi)容哪怕發(fā)生了一個(gè)字節(jié)的變動(dòng),也會(huì)導(dǎo)致整個(gè)簽名發(fā)生顯著變化。JWT 默認(rèn)使用的 HMAC SHA256 算法是一種密鑰哈希算法,適用于單體應(yīng)用中,因?yàn)榧用芎万?yàn)證都需要由同一授權(quán)服務(wù)完成。在多方或分布式應(yīng)用中,通常使用非對(duì)稱加密算法進(jìn)行簽名。這種情況下,授權(quán)服務(wù)使用私鑰簽名,并通過(guò)遵循 JSON Web Key 規(guī)范公開(kāi)一個(gè)公鑰。這個(gè)公鑰用于驗(yàn)證簽名,使其他服務(wù)能夠獨(dú)立驗(yàn)證 JWT 的真實(shí)性,無(wú)需直接與授權(quán)服務(wù)通信。
JWT 令牌的交互流程如下:
圖片
說(shuō)明:如果是在分布式環(huán)境下,通常會(huì)有單獨(dú)的認(rèn)證服務(wù)器來(lái)負(fù)責(zé)頒發(fā)令牌。
發(fā)送令牌
按照 HTTP 協(xié)議的規(guī)范,客戶端可以通過(guò)多種方式使用 HTTP 協(xié)議發(fā)送 JWT 令牌給服務(wù)端。最標(biāo)準(zhǔn)的方式是將 JWT 放在 HTTP 的 Authorization 頭部中,通常與 Bearer 方案一起使用。這種方法簡(jiǎn)單且符合 RESTful API 的最佳實(shí)踐:
GET /api/resource HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIx......
總結(jié)
JWT 令牌是分布式系統(tǒng)下憑證載體的優(yōu)秀解決方案,它優(yōu)點(diǎn)眾多:
- 解決了分布式系統(tǒng)下的狀態(tài)信息的管理問(wèn)題,讓服務(wù)端無(wú)狀態(tài),實(shí)現(xiàn)動(dòng)態(tài)擴(kuò)縮容。
- 結(jié)構(gòu)簡(jiǎn)單,輕量,憑證本身包含重要信息,服務(wù)端無(wú)需再查詢數(shù)據(jù)庫(kù)
- 通過(guò)密鑰對(duì)和簽名的方式,保證憑證信息的無(wú)法被篡改,保證了憑證的真實(shí)性
但是沒(méi)有完美的解決方案,cookie-session 的優(yōu)點(diǎn)也 JWT 也缺點(diǎn):
- 會(huì)話難以主動(dòng)失效:服務(wù)端難以注銷令牌,如果非要實(shí)現(xiàn),就要把狀態(tài)信息轉(zhuǎn)移存儲(chǔ)到 redis 中。
- 攜帶的信息有限:雖然 HTTP 沒(méi)有限制 Header 中可存儲(chǔ)的大小限制,但是 HTTP 服務(wù)端大多都有存儲(chǔ)上限,例如 tomcat 限制 8kb,nginx 限制 4kb
- 客戶端令牌泄露風(fēng)險(xiǎn):客戶端令牌存在哪里 ?Cookie ? localStrong ? Indexed ? 存在哪里都有泄露風(fēng)險(xiǎn)。只要拿到令牌就能冒認(rèn)客戶端身份
- 服務(wù)端無(wú)狀態(tài)會(huì)導(dǎo)致很多常見(jiàn)的功能難以實(shí)現(xiàn),例如:踢人下線,統(tǒng)計(jì)在線人數(shù)等等。。