SpringBoot 整合 JWT 實現(xiàn) Token 認(rèn)證

一、前言

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 原理

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)證方式存在的問題

  1. token 不能撤銷:JWT 沒有過期或者失效時脖岛,客戶端重置密碼朵栖,JWT 依然可以使用颊亮;
  2. 不支持 refresh token,JWT 過期后需要執(zhí)行登錄授權(quán)的完整流程陨溅;
  3. 無法知道用戶簽發(fā)了幾個 JWT

續(xù)篇將針對上述問題給出解決方案终惑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市声登,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌揣苏,老刑警劉巖悯嗓,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卸察,居然都是意外死亡脯厨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門坑质,熙熙樓的掌柜王于貴愁眉苦臉地迎上來合武,“玉大人,你說我怎么就攤上這事涡扼〖谔” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵吃沪,是天一觀的道長汤善。 經(jīng)常有香客問我,道長票彪,這世上最難降的妖魔是什么红淡? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮降铸,結(jié)果婚禮上在旱,老公的妹妹穿的比我還像新娘。我一直安慰自己推掸,他們只是感情好桶蝎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谅畅,像睡著了一般俊嗽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铃彰,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天绍豁,我揣著相機(jī)與錄音,去河邊找鬼牙捉。 笑死竹揍,一個胖子當(dāng)著我的面吹牛敬飒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芬位,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼无拗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了昧碉?” 一聲冷哼從身側(cè)響起英染,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎被饿,沒想到半個月后四康,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狭握,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年闪金,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片论颅。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡哎垦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恃疯,到底是詐尸還是另有隱情漏设,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布今妄,位于F島的核電站愿题,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蛙奖。R本人自食惡果不足惜潘酗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雁仲。 院中可真熱鬧仔夺,春花似錦、人聲如沸攒砖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吹艇。三九已至惰蜜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間受神,已是汗流浹背抛猖。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人财著。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓联四,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撑教。 傳聞我的和親對象是個殘疾皇子朝墩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容