一、前言
HTTP 是一個無狀態(tài)的協(xié)議薯嗤,因此服務(wù)器無法識別2次請求是否來自同一個客戶端顽爹。但在 Web 應(yīng)用中,用戶的認(rèn)證和鑒權(quán)又是非常重要的一環(huán)骆姐,實踐中產(chǎn)生了多種可用的方案镜粤,基于 Session 的會話管理即是其中一種。
在 Web 應(yīng)用發(fā)展的初期玻褪,大部分 Web 應(yīng)用采用基于 Session 的會話管理方式肉渴,其邏輯如下:
- 客戶端使用用戶名、密碼進(jìn)行認(rèn)證
- 服務(wù)端生成 Session 并存儲带射,將 SessionID 通過 Cookie 返回給客戶端
- 客戶端訪問需要認(rèn)證的接口時在 Cookie 中攜帶 SessionID
- 服務(wù)端通過 SessionID 查找 Session 并進(jìn)行鑒權(quán)同规,通過則返回給客戶端需要的數(shù)據(jù)
Cookie
Cookie 是客戶端保存用戶信息的一種機(jī)制,用來記錄用戶的一些信息窟社,也是實現(xiàn) Session 的一種方式券勺。Cookie 存儲的數(shù)據(jù)量有限,且都是保存在客戶端瀏覽器中灿里。不同的瀏覽器有不同的存儲大小关炼,但一般不超過 4KB。因此使用 Cookie 實際上只能存儲一小段的文本信息匣吊。
例如:登錄網(wǎng)站儒拂,今輸入用戶名密碼登錄了寸潦,第二天再打開很多情況下就直接打開了。這個時候用到的一個機(jī)制就是 Cookie社痛。
Session
Session 是另一種記錄客戶狀態(tài)的機(jī)制见转,它是在服務(wù)端保存的一個數(shù)據(jù)結(jié)構(gòu)(主要存儲的的 SessionID 和 Session 內(nèi)容,同時也包含了很多自定義的內(nèi)容如:用戶基礎(chǔ)信息蒜哀、權(quán)限信息斩箫、用戶機(jī)構(gòu)信息、固定變量等)撵儿,這個數(shù)據(jù)可以保存在集群校焦、數(shù)據(jù)庫、文件中统倒,用于跟蹤用戶的狀態(tài)寨典。
客戶端瀏覽器訪問服務(wù)器的時候,服務(wù)器把客戶端信息以某種形式記錄在服務(wù)器上房匆。這就是 Session耸成。客戶端瀏覽器再次訪問時只需要從該 Session 中查找該客戶的狀態(tài)就可以了浴鸿。
用戶第一次登錄后井氢,瀏覽器會將用戶信息發(fā)送給服務(wù)器,服務(wù)器會為該用戶創(chuàng)建一個 SessionId岳链,并在響應(yīng)內(nèi)容(Cookie)中將該 SessionId 一并返回給瀏覽器花竞,瀏覽器將這些數(shù)據(jù)保存在本地。當(dāng)用戶再次發(fā)送請求時掸哑,瀏覽器會自動的把上次請求存儲的 Cookie 數(shù)據(jù)自動的攜帶給服務(wù)器约急。
服務(wù)器接收到請求信息后,會通過瀏覽器請求的數(shù)據(jù)中的 SessionId 判斷當(dāng)前是哪個用戶苗分,然后根據(jù) SessionId 在 Session 庫中獲取用戶的 Session 數(shù)據(jù)返回給瀏覽器厌蔽。
例如:購物車,添加了商品之后客戶端處可以知道添加了哪些商品摔癣,而服務(wù)器端如何判別呢奴饮,所以也需要存儲一些信息就用到了 Session。
如果說 Cookie 機(jī)制是通過檢查客戶身上的“通行證”來確定客戶身份的話择浊,那么 Session 機(jī)制就是通過檢查服務(wù)器上的“客戶明細(xì)表”來確認(rèn)客戶身份戴卜。Session 相當(dāng)于程序在服務(wù)器上建立的一份客戶檔案,客戶來訪的時候只需要查詢客戶檔案表就可以了琢岩。
Session 生成后投剥,只要用戶繼續(xù)訪問,服務(wù)器就會更新 Session 的最后訪問時間粘捎,并維護(hù)該 Session薇缅。為防止內(nèi)存溢出,服務(wù)器會把長時間內(nèi)沒有活躍的 Session 從內(nèi)存刪除攒磨。這個時間就是 Session 的超時時間泳桦。如果超過了超時時間沒訪問過服務(wù)器,Session 就自動失效了娩缰。
基于 Session 的認(rèn)證方式存在如下問題:
- 服務(wù)端需要存儲 Session灸撰,由于 Session 經(jīng)常需要快速查找,通常將其存儲在內(nèi)存或內(nèi)存數(shù)據(jù)庫中拼坎,當(dāng)同時在線用戶較多時會占用大量的服務(wù)器資源浮毯;
- 在分布式架構(gòu)下,當(dāng)前訪問的節(jié)點(diǎn)可能不是創(chuàng)建 Session 的節(jié)點(diǎn)泰鸡,導(dǎo)致無法驗證债蓝,因此需要考慮在多個節(jié)點(diǎn)間同步 Session 數(shù)據(jù);
- 由于客戶端使用 Cookie 存儲 SessionID盛龄,在跨域場景下需要進(jìn)行兼容性處理饰迹,同時這種方式也難以防范 CSRF 攻擊;
- 不支持 Android余舶,IOS啊鸭,小程序等移動端;
鑒于基于 Session 的會話管理方式存在上述多個缺點(diǎn)匿值,無狀態(tài)的基于 Token 的會話管理方式誕生了赠制,所謂無狀態(tài),就是服務(wù)端不再存儲信息挟憔,甚至是不再存儲 Session钟些,其處理邏輯如下:
- 客戶端使用用戶名、密碼進(jìn)行認(rèn)證
- 服務(wù)端驗證用戶名密碼绊谭,通過后生成 Token 返回給客戶端
- 客戶端保存 Token厘唾,訪問需要認(rèn)證的接口時在 URL 參數(shù)或 HTTP Header 中加入 Token
- 服務(wù)端通過解碼 Token 進(jìn)行鑒權(quán),認(rèn)證通過則返回給客戶端需要的數(shù)據(jù)
基于 Token 的會話管理方式有效的解決了基于 Session 的會話管理方式帶來的問題:
- 服務(wù)端不需要存儲和用戶鑒權(quán)有關(guān)的信息龙誊,鑒權(quán)信息會被加密到 Token 中抚垃,服務(wù)端只需要讀取 Token 中包含的鑒權(quán)信息即可
- 避免了共享 Session 導(dǎo)致的不易擴(kuò)展問題
- 不需要依賴 Cookie,有效避免 Cookie 帶來的 CSRF 攻擊問題
- 使用 CORS 可以快速解決跨域問題
- 支持 Android趟大,IOS鹤树,小程序等不支持 Cookies 的移動端
二、什么是 JWT
JWT逊朽,全稱 JSON Web Token罕伯,是一個開放標(biāo)準(zhǔn)(RFC 7519),它以一種緊湊的叽讳、自包含的方式在各方之間安全的傳輸信息追他。其官方定義如下:
三坟募、JWT 原理
JWT 認(rèn)證原理:服務(wù)器生成一個 JWT 后會將它以 Authorization : Bearer JWT 鍵值對的形式存放在 cookies 里面發(fā)送到客戶端,客戶端再次訪問受 JWT 保護(hù)的資源時邑狸,服務(wù)器會獲取到 cookies 中存放的 JWT 信息懈糯,服務(wù)端程序首先對 Header 進(jìn)行反編碼獲取到加密算法,再通過存放在服務(wù)器上的密匙對 Header.Payload 這個字符串進(jìn)行加密单雾,然后比對 JWT 中的 Signature 和實際加密出來的結(jié)果是否一致赚哗,如果一致那么說明該 JWT 合法有效,認(rèn)證通過硅堆,否則認(rèn)證失敗屿储。
JWT格式:Header.Payload.Signature
Header
{
"typ":"JWT",
"alg":"HMAC256"
}
Header 是由上面這種格式的 Json 通過 Base64 編碼生成的字符串,它描述了編碼對象是一個 JWT 且使用 HMAC256 算法進(jìn)行加密渐逃,當(dāng)然也可以選用其他加密算法够掠。
JWT 官方類庫支持下列所有加密算法:
JWS | Algorithm | Description |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
Claim => Payload
Claim 也是一個 Json。Claim 中存放的內(nèi)容是 JWT 自身的標(biāo)準(zhǔn)屬性茄菊,所有的標(biāo)準(zhǔn)屬性都是可選的祖屏,可自行添加的,比如 JWT 的簽發(fā)者买羞、JWT 的接收者袁勺、JWT 的有效時間等;同時 Claim 中也可以存放一些自定義的屬性畜普,這個自定義的屬性可以是在用戶認(rèn)證中用于標(biāo)明用戶身份的屬性期丰,如用戶對應(yīng)的數(shù)據(jù)庫記錄 ID(為了安全起見,不可以將用戶名及密碼這類敏感的信息存放在 Claim 中)吃挑。Claim 經(jīng) Base64轉(zhuǎn)碼之后生成的一串字符串稱作Payload钝荡。 Claim 的內(nèi)容可以是:
{
loginUser: 'muyao',
userId: '10000000',
exp: 1544602234
}
Signature
將 Header 和 Claim 這兩個 Json 分別使用 Base64 方式進(jìn)行編碼,生成字符串 Header 和 Payload舶衬,然后將Header 和 Payload 以 Header.Payload 的格式拼接在一起形成一個字符串埠通,再使用 Header 中定義好的加密算法和一個密匙(這個密匙存放在服務(wù)器上,用于進(jìn)行驗證)對這個字符串進(jìn)行加密逛犹,獲得一個新的字符串端辱,這個字符串就是 Signature。
四虽画、SpringBoot 整合 JWT 實現(xiàn) Token 認(rèn)證
1. pom.xml 添加 maven 依賴
<properties>
<jwt.version>3.8.1</jwt.version>
</properties>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
2. 實現(xiàn)簽名方法和認(rèn)證方法
package com.muyao;
import java.sql.Date;
import java.util.HashMap;
import java.util.Map;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
public class JwtUtils {
/** 過期時間舞蔽,缺省15分鐘 */
private long EXPIRE_TIME = 15 * 60 * 1000;
/** token 私鑰,缺省 galaxy-all */
private String TOKEN_SECRET = "Galaxy-All";
/** header */
private Map<String, Object> header = new HashMap<>();
/** 簽名算法實例 */
private Algorithm algorithm;
/** token 認(rèn)證器 */
private JWTVerifier verifier;
public JwtUtils() {
JwtInit();
}
public JwtUtils(long expireTime, String tokenSecret) {
this.EXPIRE_TIME = expireTime;
this.TOKEN_SECRET = tokenSecret;
JwtInit();
}
// 簽名算法和認(rèn)證器初始化
private void JwtInit() {
this.algorithm = Algorithm.HMAC256(this.TOKEN_SECRET);
this.verifier = JWT.require(this.algorithm).build();
this.header.put("typ", "JWT");
this.header.put("alg", "HS256");
}
/**
* 簽名方法:采用 HMAC256算法码撰,附帶 claims 信息生成簽名
*
* @param claims
* @return
*/
public String sign(Map<String, String> claims) throws Exception {
// 計算 token 過期時間
Date date = new Date(System.currentTimeMillis() + this.EXPIRE_TIME);
try {
JWTCreator.Builder jwt = JWT.create().withHeader(this.header).withExpiresAt(date);
for (Map.Entry<String, String> entry : claims.entrySet()) {
jwt.withClaim(entry.getKey(), entry.getValue());
}
return jwt.sign(this.algorithm);
} catch (JWTCreationException exception) {
exception.printStackTrace();
throw new Exception(String.format("生成簽名異成粒【%s】!", exception.getMessage()));
}
}
/**
* 認(rèn)證方法類
* @param token
* @return
*/
public boolean verify(String token) {
try {
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
return false;
}
}
}
五、JWT 認(rèn)證方式存在的問題
- token 不能撤銷:JWT 沒有過期或者失效時脖岛,客戶端重置密碼朵栖,JWT 依然可以使用颊亮;
- 不支持 refresh token,JWT 過期后需要執(zhí)行登錄授權(quán)的完整流程陨溅;
- 無法知道用戶簽發(fā)了幾個 JWT
續(xù)篇將針對上述問題給出解決方案终惑。