一、前言
大家好呀,我是summo,之前有自學過Shrio框架,網上一搜就有SpringBoot整合Shrio+ JWT的文章,我是在學習Shrio框架的時候順帶學的JWT。後來我還看見有很多博主專門寫文章介紹JWT,說這個東西的優點很多,安全性好、去中心化、方便啥的,我就把JWT也應用在我們自己的系統中了。但最近發現這玩意越來越讓我覺得彆扭,總感覺哪裡不太對勁,重新審查我的登入認證邏輯之後才發現:我不應該用JWT的!
這裡我用一句解釋不該用的原因,省得浪費大家的時間:我的系統有Redis,而且還用Redis存了JWT,隨著系統升級,JWT越來越像普通Token!
明白原理的同學可能心中暗笑,直接跳過看下一篇了,不明白原理的同學,可以看看這個四不像是怎麼被我搭出來的。
二、JWT是什麼?
看我文章的有很多大神,也有一些小白,所以為了不讓小白們看的雲裡霧裡,我還是有必要介紹一些基本原理。JWT是 JSON Web Token 的縮寫,可以對JSON物件進行編碼(加密),並透過這個編碼傳遞資訊。
1. JWT的結構
(1)頭部(Header)
頭部通常由兩部分組成,即令牌的型別(typ)和所使用的演算法(alg)。例如,一個頭部可能是 {"alg": "HS256", "typ": "JWT"},表示使用 HMAC SHA-256 演算法對令牌進行簽名。
(2)載荷(Payload)
載荷包含了 JWT 的宣告資訊,用於描述令牌的相關內容。載荷可以包含標準宣告(例如:發行者、主題、過期時間等),也可以包含自定義宣告。例如,一個載荷可能是 {"sub": "1234567890", "name": "John Doe", "exp": 1516239022}。
(3)簽名(Signature)
簽名用於驗證令牌的完整性和真實性。簽名通常由頭部、載荷和金鑰一起計算而得。驗證者可以使用相同的金鑰重新計算簽名,並將結果與令牌中的簽名進行比較,以確認令牌的真實性。
2. SpringBoot使用JWT
(1)maven引入
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
(2)程式碼示例
import java.util.Date;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@Slf4j
public class JWTUtil {
/**
* 過期時間
*/
private static final long EXPIRE_TIME = 60 * 1000;
/**
* 校驗 token是否正確
*
* @param token 金鑰
* @param secret 使用者的密碼
* @return 是否正確
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
verifier.verify(token);
log.info("token is valid");
return true;
} catch (Exception e) {
log.info("token is invalid{}", e.getMessage());
return false;
}
}
/**
* 從 token中獲取使用者名稱
*
* @return token中包含的使用者名稱
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.error("error:{}", e.getMessage());
return null;
}
}
/**
* 生成 token
*
* @param username 使用者名稱
* @param secret 使用者的密碼
* @return token
*/
public static String sign(String username, String secret) {
try {
username = StringUtils.lowerCase(username);
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
log.error("error:{}", e);
return null;
}
}
public static void main(String[] args) {
//對資料進行加密
String token = sign("zhangshan", "123456");
System.out.println(token);
//對資料進行解密
System.out.println(getUsername(token));
}
}
執行一下
JWT還是很簡單的,一學就會,很多博主在介紹它的時候都會說它安全、方便、去中心化等等,然後強烈推薦大家使用。但這裡我要就要給大家潑冷水了,學是肯定要學的,用就需要看情況了,不能別人說它好,你就無腦用,然後用成一個四不像。至於我為什麼說我用來是四不像,接著看!
三、JWT vs Token+Redis
在設計no session系統時,有兩種可選方案:JWT與Token+Redis。
1. 原理簡介
-
JWT: 生成併發給客戶端之後,後臺是不用儲存,客戶端訪問時會驗證其簽名、過期時間等再取出裡面的資訊(如username),再使用該資訊直接查詢使用者資訊完成登入驗證。jwt自帶簽名、過期等校驗,後臺不用儲存,缺陷是一旦下發,服務後臺無法拒絕攜帶該jwt的請求(如踢除使用者);
-
Token+Redis: 是自己生成個32位的key,value為使用者資訊,訪問時判斷redis裡是否有該token,如果有,則載入該使用者資訊完成登入。服務需要儲存下發的每個token及對應的value,維持其過期時間,好處是隨時可以刪除某個token,阻斷該token繼續使用。
2. 兩種方案的優缺點
(1)去中心化的JWT
優點:
- 去中心化,便於分散式系統使用
- 基本資訊可以直接放在token中,如id,name。
- 功能許可權較少的話,可以直接放在token中。用bit位表示使用者所具有的功能許可權
缺點:
- 服務端不能主動讓token失效
(2)中心化的Redis+Token
優點:
- 服務端可以主動讓token失效
缺點:
- 依賴記憶體或redis儲存。
- 分散式系統的話,需要redis呼叫增加了系統複雜性。
光看優缺點的話,JWT優點還比Redis+Token多,在小白時期的我一看:好傢伙,JWT這麼多優點,用它準沒錯,看來簡歷上又可以加上一筆了!
四、我的方案
想法是美好的,但現實是殘酷的,為什麼我會覺得越來越彆扭,先上一張流程圖,讓大家看看我在業務中是怎麼做的,如下:
上面的方案是JWT或者Redis+Token,而我的方案是JWT+Redis🤡。和普通的流程相比,我還加了一個加解密流程,因為我覺得JWT資料格式太明顯了,一眼就知道是用的是什麼認證方式,容易被篡改。你說這個一點用都沒有嗎?好像還有那麼點用,最起碼JWT變得更安全了...
1. 彆扭原因
(1)多餘的加解密流程
雖然給JWT加一下密提高了安全性,但是導致JWT的自帶的過期機制失效了,必須得加上Redis的快取失效機制,在安全和方便的選項中我選擇了安全。
(2)系統對使用者的操作頻繁
因為我們的系統是一個傳統的管理端+C端模式,管理員經常給使用者增刪許可權,剛好命中“服務端不能主動讓token失效”這一缺陷,這是最大的原因。
(3)JWT隨著系統的升級字元越來越長
JWT儲存的資訊比較少的時候,還只有一兩百個字元,但是欄位一多,直接變成“小作文”,我現在看著這一段長長的token,頭都大。
(4)單點登入使用JWT太不可控了
實現不了單點登入之後的單點退出功能,即使用者在一個系統登出,所有相關係統也自動登出,JWT完全不能滿足這個需求。
2. 總結一下
寫這篇文章我想肯定會有很多人不服,說我不會用就說人家不好用。誠然,我確實不太會用,不然我也不會用JWT到我的系統中來。我也看了很多介紹JWT的文章,都是說原理和優點,很少有說它的應用場景,知乎上有一篇關於jwt與token+redis,哪種方案更好用?的辯論,很激烈,感興趣的同學可以去看看。
這個帖子裡面的大部分人都認為JWT不是一個必要的元件,甚至有人說任何時候都不應該首要考慮JWT
,現在的我深感贊同。裡面還有不少老哥推薦使用折中方案,也就是上面我的方案:JWT+Redis,不過我總感覺這種用法很彆扭,因為一旦上了Redis那和Redis+Token方案還有什麼不同,而且JWT還長的多,白白浪費使用者的流量。
裡面有個老哥的比喻我覺得非常形象,給大家看下
我覺得這個老哥講的還挺有道理,JWT本身也只是一個token(有點長的token),非要讓它滿足所有的需求屬實是難為它了。但是如果僅僅只是創造一個新品,如義大利麵,就開始瘋狂吹噓它無所不能,給小白一些錯誤的引導,那就不對了,畢竟喜歡吃義大利麵的人也不多。
文末小彩蛋,自建摸魚網站,各大網站熱搜一覽,上班和摸魚很配哦!