我不應該用JWT的!

sum墨發表於2024-07-15

一、前言

大家好呀,我是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

優點

  1. 去中心化,便於分散式系統使用
  2. 基本資訊可以直接放在token中,如id,name。
  3. 功能許可權較少的話,可以直接放在token中。用bit位表示使用者所具有的功能許可權

缺點

  1. 服務端不能主動讓token失效

(2)中心化的Redis+Token

優點

  1. 服務端可以主動讓token失效

缺點

  1. 依賴記憶體或redis儲存。
  2. 分散式系統的話,需要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),非要讓它滿足所有的需求屬實是難為它了。但是如果僅僅只是創造一個新品,如義大利麵,就開始瘋狂吹噓它無所不能,給小白一些錯誤的引導,那就不對了,畢竟喜歡吃義大利麵的人也不多。

文末小彩蛋,自建摸魚網站,各大網站熱搜一覽,上班和摸魚很配哦!

相關文章