最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot中實(shí)現(xiàn)JWT登錄授權(quán)+無(wú)感刷新全流程

 更新時(shí)間:2026年04月08日 08:33:39   作者:小碼哥_常  
在當(dāng)今的分布式系統(tǒng)和前后端分離架構(gòu)盛行的時(shí)代,傳統(tǒng)的基于?Session?的認(rèn)證方式逐漸暴露出諸多弊端,而?JWT的出現(xiàn),猶如一道曙光,為這些問(wèn)題提供了完美的解決方案,本文將詳細(xì)地為大家講解?Spring?Boot?整合?JWT?實(shí)現(xiàn)登錄認(rèn)證與接口授權(quán)的全流程

一、引言

在當(dāng)今的分布式系統(tǒng)和前后端分離架構(gòu)盛行的時(shí)代,傳統(tǒng)的基于 Session 的認(rèn)證方式逐漸暴露出諸多弊端。想象一下,在一個(gè)大型電商系統(tǒng)中,用戶的操作頻繁涉及多個(gè)服務(wù)模塊,且前端可能是網(wǎng)頁(yè)端、移動(dòng)端等多種類型。若采用傳統(tǒng) Session 認(rèn)證,當(dāng)用戶從網(wǎng)頁(yè)端切換到移動(dòng)端繼續(xù)操作時(shí),由于跨域問(wèn)題,Session 信息難以有效傳遞 ,導(dǎo)致用戶需要重新登錄。同時(shí),隨著用戶數(shù)量的急劇增加,服務(wù)器需要存儲(chǔ)大量的 Session 信息,這無(wú)疑給服務(wù)器帶來(lái)了沉重的存儲(chǔ)壓力,就像一間小倉(cāng)庫(kù)要存放海量的貨物,空間遲早會(huì)被耗盡。

而 JWT(JSON Web Token)的出現(xiàn),猶如一道曙光,為這些問(wèn)題提供了完美的解決方案。JWT 是一種輕量級(jí)的身份認(rèn)證與授權(quán)方案,具有無(wú)狀態(tài)的特性,這意味著服務(wù)器無(wú)需存儲(chǔ)用戶的會(huì)話信息,大大減輕了服務(wù)器的負(fù)擔(dān),如同給服務(wù)器卸下了沉重的包袱。它在跨域場(chǎng)景下表現(xiàn)出色,能夠輕松地在不同的前端應(yīng)用和后端服務(wù)之間傳遞,為前后端分離架構(gòu)的發(fā)展提供了有力支持。并且,JWT 易于擴(kuò)展,方便與各種系統(tǒng)集成,無(wú)論是小型項(xiàng)目還是大型企業(yè)級(jí)應(yīng)用,都能發(fā)揮其優(yōu)勢(shì)。

本文將詳細(xì)地為大家講解 Spring Boot 整合 JWT 實(shí)現(xiàn)登錄認(rèn)證與接口授權(quán)的全流程,從最基礎(chǔ)的環(huán)境搭建,到核心功能的實(shí)現(xiàn),再到進(jìn)階優(yōu)化,每一步都有詳細(xì)的代碼示例和解釋,讓你輕松掌握這一關(guān)鍵技術(shù),為你的項(xiàng)目開(kāi)發(fā)保駕護(hù)航。

二、JWT 基礎(chǔ)掃盲

2.1 JWT 是什么

JWT,即 JSON Web Token,是一種基于 JSON 的開(kāi)放標(biāo)準(zhǔn)(RFC 7519) ,用于在網(wǎng)絡(luò)應(yīng)用間安全地傳輸聲明。簡(jiǎn)單來(lái)說(shuō),它是一種輕量級(jí)的身份認(rèn)證和授權(quán)方案,以 JSON 格式組織和傳輸信息。相較于傳統(tǒng)的認(rèn)證方式,JWT 具有無(wú)狀態(tài)、自包含的特性,這意味著服務(wù)器無(wú)需存儲(chǔ)用戶的會(huì)話信息,減輕了服務(wù)器的負(fù)載,同時(shí)也方便在不同的服務(wù)和系統(tǒng)之間傳遞身份驗(yàn)證信息,就像一個(gè)小巧且功能強(qiáng)大的通行證,在分布式系統(tǒng)和前后端分離的架構(gòu)中被廣泛應(yīng)用。

2.2 JWT 結(jié)構(gòu)剖析

JWT 看起來(lái)是一個(gè)很長(zhǎng)的字符串,實(shí)際上它由三部分組成,每部分之間用英文句點(diǎn) “.” 分隔,即 Header.Payload.Signature。

  • Header(頭部):主要存儲(chǔ)兩方面信息,一是令牌的類型,通常就是 “JWT”;二是簽名算法,常見(jiàn)的如 HMAC SHA256、RSA 等 。例如:
{
  "alg": "HS256",
  "typ": "JWT"
}

將這個(gè) JSON 對(duì)象進(jìn)行 Base64Url 編碼后,就成為了 JWT 的第一部分。

  • Payload(載荷):存放實(shí)際需要傳遞的聲明(claims)信息。這些聲明可以分為三類:已注冊(cè)聲明(如 iss 簽發(fā)者、exp 過(guò)期時(shí)間、iat 簽發(fā)時(shí)間等)、公共聲明(開(kāi)發(fā)者自定義的公開(kāi)信息,像用戶 ID、角色等)、私有聲明(應(yīng)用內(nèi)自定義的非公開(kāi)信息)。例如:
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022,
  "exp": 1516239022 + 3600 // 假設(shè)有效期1小時(shí)
}

同樣,將這個(gè) JSON 對(duì)象 Base64Url 編碼后,構(gòu)成 JWT 的第二部分。需要注意的是,Payload 默認(rèn)不加密,不要存放敏感信息,如密碼等。

  • Signature(簽名):用于驗(yàn)證 JWT 的完整性,確保內(nèi)容未被篡改。生成簽名需要用到編碼后的 Header、編碼后的 Payload、一個(gè)只有服務(wù)器知道的密鑰(secret)以及 Header 中指定的簽名算法。以 HS256 算法為例,簽名生成公式為:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

計(jì)算出的簽名作為 JWT 的第三部分,與前兩部分共同組成完整的 JWT,如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 。

2.3 認(rèn)證流程詳解

  1. 用戶登錄:用戶在客戶端(如瀏覽器、移動(dòng)應(yīng)用)輸入用戶名和密碼,向服務(wù)器發(fā)起登錄請(qǐng)求。
  2. 服務(wù)器驗(yàn)證:服務(wù)器接收到登錄請(qǐng)求后,驗(yàn)證用戶名和密碼是否正確。若驗(yàn)證通過(guò),根據(jù)用戶信息生成 JWT。這個(gè)過(guò)程就像是服務(wù)器給用戶頒發(fā)了一張通行證,通行證里包含了用戶的關(guān)鍵信息(如用戶 ID、角色等),并使用密鑰和特定算法進(jìn)行了簽名。
  3. 返回 JWT:服務(wù)器將生成的 JWT 返回給客戶端??蛻舳私邮盏?JWT 后,可以將其存儲(chǔ)在本地,常見(jiàn)的存儲(chǔ)方式有 localStorage、sessionStorage 或者 HttpOnly Cookie 等 。
  4. 后續(xù)請(qǐng)求:在后續(xù)的每一次請(qǐng)求中,客戶端都會(huì)將 JWT 攜帶在 HTTP 請(qǐng)求頭中,一般是放在 Authorization 字段,格式為 Authorization: Bearer <JWT> 。Bearer 表示認(rèn)證方案,告訴服務(wù)器使用 JWT 進(jìn)行認(rèn)證。
  5. 服務(wù)器驗(yàn)證:服務(wù)器接收到請(qǐng)求后,從請(qǐng)求頭中提取 JWT,然后使用相同的密鑰和簽名算法對(duì) JWT 進(jìn)行驗(yàn)證。驗(yàn)證內(nèi)容包括簽名是否正確、JWT 是否過(guò)期等。如果驗(yàn)證通過(guò),服務(wù)器就認(rèn)為該請(qǐng)求是合法的,并且可以從 JWT 的 Payload 中獲取用戶相關(guān)信息,從而進(jìn)行相應(yīng)的授權(quán)操作,返回請(qǐng)求的資源;若驗(yàn)證失敗,則返回 401 Unauthorized 錯(cuò)誤,拒絕訪問(wèn) 。

三、Spring Boot 環(huán)境搭建

3.1 核心依賴引入

在 Spring Boot 項(xiàng)目中,首先要引入關(guān)鍵依賴,為后續(xù)使用 JWT 進(jìn)行登錄授權(quán)奠定基礎(chǔ)。這些依賴就像是搭建房屋的基石,缺一不可。我們需要添加 Spring Security,它是 Spring 生態(tài)中提供強(qiáng)大認(rèn)證授權(quán)框架的組件,能為應(yīng)用程序保駕護(hù)航,確保只有合法用戶才能訪問(wèn)特定資源,就像小區(qū)門(mén)口嚴(yán)格的保安,阻擋外來(lái)人員隨意進(jìn)入;JJWT 是 Java 領(lǐng)域主流的 JWT 工具庫(kù),專門(mén)用于生成、解析和驗(yàn)證 JWT,為我們處理 JWT 相關(guān)操作提供了便捷的方法;Spring Web 則用于編寫(xiě)接口,方便我們測(cè)試整個(gè)認(rèn)證流程,讓我們能直觀地看到認(rèn)證授權(quán)在實(shí)際接口調(diào)用中的效果。

如果你的項(xiàng)目使用 Maven 構(gòu)建,在pom.xml文件中添加以下依賴配置:

<dependencies>
    <!-- Spring Security依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- JJWT依賴 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.2</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId> 
        <version>0.11.2</version> 
        <scope>runtime</scope> 
    </dependency>
    <!-- Spring Web依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.2 配置文件設(shè)置

配置文件在整個(gè)項(xiàng)目中起著至關(guān)重要的作用,它就像是項(xiàng)目的 “指揮中心”,通過(guò)設(shè)置各種參數(shù),我們可以靈活地調(diào)整項(xiàng)目的運(yùn)行方式。在application.propertiesapplication.yml文件中,需要配置 JWT 的一些關(guān)鍵參數(shù)。

比如,設(shè)置 JWT 的密鑰(jwt.secret),這個(gè)密鑰是生成和驗(yàn)證 JWT 簽名的關(guān)鍵,務(wù)必妥善保管,就像保管自己家門(mén)的鑰匙一樣重要,一旦泄露,整個(gè)認(rèn)證體系將面臨被攻破的風(fēng)險(xiǎn);設(shè)置訪問(wèn)令牌過(guò)期時(shí)間(jwt.access-token-expiration),根據(jù)業(yè)務(wù)需求,合理設(shè)定訪問(wèn)令牌的有效時(shí)長(zhǎng),比如可以設(shè)置為 30 分鐘,這樣既能保證一定的安全性,又不會(huì)頻繁讓用戶重新登錄;還有刷新令牌過(guò)期時(shí)間(jwt.refresh-token-expiration),通常刷新令牌的過(guò)期時(shí)間會(huì)比訪問(wèn)令牌長(zhǎng)很多,比如設(shè)置為 7 天,用于在訪問(wèn)令牌過(guò)期時(shí),無(wú)需用戶重新輸入用戶名和密碼,就能無(wú)感刷新獲取新的訪問(wèn)令牌。

application.yml為例,配置如下:

jwt:
  secret: your-secret-key
  access-token-expiration: 1800000  # 30分鐘,單位毫秒
  refresh-token-expiration: 604800000 # 7天,單位毫秒

通過(guò)上述環(huán)境搭建步驟,我們的 Spring Boot 項(xiàng)目已經(jīng)具備了使用 JWT 進(jìn)行登錄授權(quán)的基本條件,接下來(lái)就可以著手實(shí)現(xiàn)核心的登錄認(rèn)證和接口授權(quán)功能了。

四、核心功能實(shí)現(xiàn)

4.1 JWT 工具類封裝

在 Spring Boot 項(xiàng)目中,為了方便地處理 JWT 相關(guān)操作,我們需要將常用的 JWT 操作封裝成一個(gè)工具類,就像把各種工具整理到一個(gè)工具箱里,使用時(shí)隨手可拿。這里創(chuàng)建一個(gè)JwtUtils類,利用 Spring 的依賴注入機(jī)制,將 JWT 的密鑰和過(guò)期時(shí)間等配置信息注入到類中,這樣我們就能靈活地根據(jù)配置生成和驗(yàn)證 JWT,而無(wú)需在代碼中硬編碼這些關(guān)鍵信息。

首先,在類中使用@Component注解,將JwtUtils標(biāo)記為 Spring 組件,這樣 Spring 容器在啟動(dòng)時(shí)會(huì)自動(dòng)掃描并實(shí)例化這個(gè)類,使其成為容器管理的 Bean,方便在其他組件中通過(guò)依賴注入的方式使用。接著,通過(guò)@Value注解從配置文件中讀取 JWT 的密鑰(jwt.secret)、訪問(wèn)令牌過(guò)期時(shí)間(jwt.access-token-expiration)和刷新令牌過(guò)期時(shí)間(jwt.refresh-token-expiration) ,并將這些值賦給相應(yīng)的成員變量。

下面是JwtUtils類中幾個(gè)關(guān)鍵方法的實(shí)現(xiàn)及解釋:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.access-token-expiration}")
    private long accessTokenExpiration;
    @Value("${jwt.refresh-token-expiration}")
    private long refreshTokenExpiration;
    /**
     * 生成訪問(wèn)令牌
     * @param username 用戶名
     * @return 生成的訪問(wèn)令牌
     */
    public String generateAccessToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return generateToken(claims, username, accessTokenExpiration);
    }
    /**
     * 生成刷新令牌
     * @param username 用戶名
     * @return 生成的刷新令牌
     */
    public String generateRefreshToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return generateToken(claims, username, refreshTokenExpiration);
    }
    /**
     * 生成JWT令牌
     * @param claims 負(fù)載信息
     * @param subject 主題,一般為用戶名
     * @param expireTime 過(guò)期時(shí)間(毫秒)
     * @return 生成的JWT令牌
     */
    private String generateToken(Map<String, Object> claims, String subject, long expireTime) {
        Key key = Keys.hmacShaKeyFor(secret.getBytes());
        return Jwts.builder()
               .setClaims(claims)
               .setSubject(subject)
               .setIssuedAt(new Date())
               .setExpiration(new Date(System.currentTimeMillis() + expireTime))
               .signWith(key, SignatureAlgorithm.HS256)
               .compact();
    }
    /**
     * 從令牌中獲取用戶名
     * @param token JWT令牌
     * @return 用戶名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getSubject();
    }
    /**
     * 驗(yàn)證令牌是否有效
     * @param token JWT令牌
     * @return 是否有效
     */
    public boolean validateToken(String token) {
        try {
            Key key = Keys.hmacShaKeyFor(secret.getBytes());
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 判斷令牌是否過(guò)期
     * @param token JWT令牌
     * @return 是否過(guò)期
     */
    public boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();
        return expiration.before(new Date());
    }
    /**
     * 從令牌中獲取負(fù)載信息
     * @param token JWT令牌
     * @return 負(fù)載信息
     */
    private Claims getClaimsFromToken(String token) {
        Key key = Keys.hmacShaKeyFor(secret.getBytes());
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}
  • 生成訪問(wèn)令牌( generateAccessToken :該方法接收用戶名作為參數(shù),創(chuàng)建一個(gè)空的claims(載荷)Map,然后調(diào)用generateToken方法,傳入claims、用戶名和訪問(wèn)令牌過(guò)期時(shí)間,生成訪問(wèn)令牌。這里空的claims可以在后續(xù)根據(jù)業(yè)務(wù)需求添加更多用戶相關(guān)信息,比如用戶角色、用戶 ID 等 ,使令牌攜帶更豐富的用戶身份信息。
  • 生成刷新令牌( generateRefreshToken :與生成訪問(wèn)令牌類似,只是傳入的過(guò)期時(shí)間是刷新令牌的過(guò)期時(shí)間,用于生成長(zhǎng)期有效的刷新令牌,以便在訪問(wèn)令牌過(guò)期時(shí),用戶無(wú)需重新輸入用戶名和密碼,就能獲取新的訪問(wèn)令牌,提升用戶體驗(yàn)。
  • 生成 JWT 令牌( generateToken :這是一個(gè)私有的核心方法,用于實(shí)際生成 JWT 令牌。它接收claims(載荷信息)、subject(主題,通常是用戶名)和expireTime(過(guò)期時(shí)間,單位毫秒)作為參數(shù)。首先根據(jù)密鑰生成一個(gè)Key對(duì)象,然后使用Jwts.builder構(gòu)建 JWT。依次設(shè)置claims、subject、簽發(fā)時(shí)間(當(dāng)前時(shí)間)、過(guò)期時(shí)間,并使用指定的SignatureAlgorithm.HS256算法和密鑰進(jìn)行簽名,最后調(diào)用compact方法生成緊湊的 JWT 字符串 。
  • 從令牌中獲取用戶名( getUsernameFromToken :該方法從 JWT 令牌中提取出用戶名。首先調(diào)用getClaimsFromToken方法獲取令牌的claims(載荷),然后通過(guò)claims.getSubject()獲取主題,即用戶名,這樣在驗(yàn)證令牌后,我們就能方便地獲取到令牌對(duì)應(yīng)的用戶身份。
  • 驗(yàn)證令牌是否有效( validateToken :嘗試使用密鑰和簽名算法解析 JWT 令牌,如果解析成功,說(shuō)明令牌有效,返回true;如果在解析過(guò)程中出現(xiàn)異常,如簽名驗(yàn)證失敗、令牌格式錯(cuò)誤、令牌已過(guò)期等,說(shuō)明令牌無(wú)效,返回false,確保只有合法有效的令牌才能通過(guò)驗(yàn)證,保障系統(tǒng)安全。
  • 判斷令牌是否過(guò)期( isTokenExpired :從令牌中獲取claims(載荷),提取其中的過(guò)期時(shí)間expiration,然后與當(dāng)前時(shí)間進(jìn)行比較,如果過(guò)期時(shí)間早于當(dāng)前時(shí)間,說(shuō)明令牌已過(guò)期,返回true,否則返回false,這在處理令牌過(guò)期邏輯時(shí)非常重要,比如決定是否需要刷新令牌。
  • 從令牌中獲取負(fù)載信息( getClaimsFromToken :使用密鑰和簽名算法解析 JWT 令牌,返回解析后的claims(載荷),其中包含了用戶相關(guān)的各種信息,如用戶名、角色、過(guò)期時(shí)間等,為后續(xù)根據(jù)令牌獲取用戶信息提供了基礎(chǔ)。

通過(guò)以上封裝,JwtUtils類提供了一套完整且便捷的 JWT 操作方法,在整個(gè)項(xiàng)目中,無(wú)論是生成令牌、驗(yàn)證令牌還是從令牌中提取信息,都可以通過(guò)調(diào)用這個(gè)工具類的方法輕松實(shí)現(xiàn),大大提高了代碼的復(fù)用性和可維護(hù)性 。

4.2 實(shí)現(xiàn)認(rèn)證過(guò)濾器

在 Spring Boot 項(xiàng)目中,為了對(duì)每個(gè)請(qǐng)求進(jìn)行 JWT 認(rèn)證,我們需要?jiǎng)?chuàng)建一個(gè)認(rèn)證過(guò)濾器。這個(gè)過(guò)濾器就像是一個(gè)嚴(yán)格的保安,站在請(qǐng)求進(jìn)入系統(tǒng)的入口,對(duì)每個(gè)請(qǐng)求進(jìn)行檢查,只有持有合法 JWT 令牌的請(qǐng)求才能放行進(jìn)入系統(tǒng)。

這里創(chuàng)建一個(gè)JwtAuthenticationFilter類,讓它繼承OncePerRequestFilterOncePerRequestFilter保證每個(gè)請(qǐng)求只會(huì)被過(guò)濾一次,避免重復(fù)過(guò)濾帶來(lái)的性能損耗。在這個(gè)類中,注入JwtUtils工具類,用于驗(yàn)證 JWT 令牌的有效性,同時(shí)注入UserDetailsService,以便在驗(yàn)證令牌通過(guò)后,獲取用戶的詳細(xì)信息,進(jìn)行后續(xù)的授權(quán)操作。

下面是JwtAuthenticationFilter類的核心實(shí)現(xiàn)及邏輯解釋:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 跳過(guò)登錄和刷新令牌的接口
        String requestURI = request.getRequestURI();
        if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
            chain.doFilter(request, response);
            return;
        }

        // 從請(qǐng)求頭獲取token
        String token = getTokenFromRequest(request);

        if (token != null && jwtUtils.validateToken(token)) {
            // 從token中獲取用戶名
            String username = jwtUtils.getUsernameFromToken(token);

            // 從UserDetailsService中獲取用戶詳細(xì)信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // 創(chuàng)建認(rèn)證對(duì)象
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
            );
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // 將認(rèn)證對(duì)象存入SecurityContextHolder
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        chain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
  • 跳過(guò)特定接口:在doFilterInternal方法中,首先獲取當(dāng)前請(qǐng)求的 URI,判斷是否是登錄接口(/api/auth/login)或刷新令牌接口(/api/auth/refresh) 。如果是這兩個(gè)接口之一,直接調(diào)用chain.doFilter(request, response)放行請(qǐng)求,因?yàn)榈卿浗涌谟糜谟脩臬@取 JWT 令牌,刷新令牌接口用于在訪問(wèn)令牌過(guò)期時(shí)獲取新的訪問(wèn)令牌,這兩個(gè)接口在請(qǐng)求時(shí)不需要進(jìn)行 JWT 認(rèn)證,避免循環(huán)認(rèn)證問(wèn)題。
  • 從請(qǐng)求頭獲取 token:調(diào)用getTokenFromRequest方法從請(qǐng)求頭中提取 JWT 令牌。該方法先獲取請(qǐng)求頭中的Authorization字段,這是約定俗成用于傳遞認(rèn)證信息的字段。如果Authorization字段存在且以Bearer 開(kāi)頭(Bearer 是一種常見(jiàn)的認(rèn)證方案前綴,表示使用令牌認(rèn)證),則截取Bearer 后面的字符串,即 JWT 令牌,返回給調(diào)用者;如果不符合條件,返回null
  • 驗(yàn)證 token 并設(shè)置認(rèn)證信息:當(dāng)獲取到 JWT 令牌后,調(diào)用jwtUtils.validateToken(token)方法驗(yàn)證令牌的有效性。如果令牌有效,通過(guò)jwtUtils.getUsernameFromToken(token)從令牌中提取用戶名。接著,利用注入的UserDetailsService,調(diào)用loadUserByUsername(username)方法,根據(jù)用戶名從數(shù)據(jù)庫(kù)或其他數(shù)據(jù)源中加載用戶的詳細(xì)信息,包括用戶名、密碼(在驗(yàn)證過(guò)程中可能用到)、用戶權(quán)限等 。然后,創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken認(rèn)證對(duì)象,將用戶詳細(xì)信息、null(密碼在驗(yàn)證令牌后不再需要傳遞,這里設(shè)為null)和用戶權(quán)限傳入構(gòu)造函數(shù)。再調(diào)用setDetails方法,設(shè)置認(rèn)證對(duì)象的詳細(xì)信息,這里使用WebAuthenticationDetailsSource創(chuàng)建請(qǐng)求相關(guān)的詳細(xì)信息。最后,將這個(gè)認(rèn)證對(duì)象存入SecurityContextHolderSecurityContextHolder是 Spring Security 用于存儲(chǔ)當(dāng)前認(rèn)證信息的地方,這樣在后續(xù)的請(qǐng)求處理過(guò)程中,其他組件就可以從這里獲取到當(dāng)前用戶的認(rèn)證信息,進(jìn)行相應(yīng)的授權(quán)操作 。
  • 放行請(qǐng)求:在完成上述處理后,無(wú)論是否成功驗(yàn)證令牌,都調(diào)用chain.doFilter(request, response)將請(qǐng)求傳遞給下一個(gè)過(guò)濾器或處理器,繼續(xù)處理請(qǐng)求。如果令牌驗(yàn)證成功,后續(xù)組件可以基于已設(shè)置的認(rèn)證信息進(jìn)行授權(quán)操作;如果令牌驗(yàn)證失敗,由于沒(méi)有在SecurityContextHolder中設(shè)置有效的認(rèn)證信息,后續(xù)的授權(quán)操作會(huì)因?yàn)檎J(rèn)證失敗而拒絕請(qǐng)求 。

通過(guò)JwtAuthenticationFilter的實(shí)現(xiàn),我們?cè)?Spring Boot 項(xiàng)目中建立了一個(gè)有效的 JWT 認(rèn)證機(jī)制,對(duì)每個(gè)進(jìn)入系統(tǒng)的請(qǐng)求進(jìn)行嚴(yán)格的身份驗(yàn)證,確保只有合法用戶的請(qǐng)求才能訪問(wèn)受保護(hù)的資源,大大提高了系統(tǒng)的安全性和可靠性 。

五、無(wú)感刷新機(jī)制實(shí)現(xiàn)

5.1 雙 Token 機(jī)制原理

在實(shí)際應(yīng)用中,為了提升用戶體驗(yàn),同時(shí)保障系統(tǒng)安全性,我們引入雙 Token 機(jī)制,即同時(shí)使用 Access Token(訪問(wèn)令牌)和 Refresh Token(刷新令牌)。Access Token 主要用于用戶在正常操作過(guò)程中,每次請(qǐng)求時(shí)攜帶進(jìn)行身份驗(yàn)證,它包含了用戶的關(guān)鍵信息,如用戶名、用戶 ID、角色等,這些信息會(huì)在服務(wù)器驗(yàn)證令牌時(shí)被提取和使用,以確認(rèn)請(qǐng)求的合法性和用戶的權(quán)限。但由于其在網(wǎng)絡(luò)傳輸中頻繁使用,一旦泄露,可能導(dǎo)致用戶身份被冒用,所以通常設(shè)置較短的有效期,比如 30 分鐘 ,這就像一把有效期很短的臨時(shí)鑰匙,即使丟失,被他人利用的時(shí)間也有限。

而 Refresh Token 則是專門(mén)用于在 Access Token 過(guò)期時(shí),獲取新的 Access Token,它就像是一把備用鑰匙,有效期相對(duì)較長(zhǎng),例如可以設(shè)置為 7 天。因?yàn)樗恢苯訁⑴c業(yè)務(wù)接口的訪問(wèn)認(rèn)證,使用頻率較低,所以泄露的風(fēng)險(xiǎn)相對(duì)較小,一般會(huì)存儲(chǔ)在相對(duì)安全的地方,如 HttpOnly Cookie 中,防止前端 JavaScript 代碼直接訪問(wèn),避免被 XSS 攻擊竊取 。

當(dāng)用戶登錄成功后,服務(wù)器會(huì)同時(shí)生成 Access Token 和 Refresh Token 并返回給客戶端??蛻舳嗽诤罄m(xù)請(qǐng)求時(shí),會(huì)將 Access Token 攜帶在 HTTP 請(qǐng)求頭中發(fā)送給服務(wù)器。服務(wù)器在接收到請(qǐng)求后,首先驗(yàn)證 Access Token 的有效性。如果 Access Token 有效,正常處理請(qǐng)求;若 Access Token 過(guò)期,但此時(shí)客戶端還持有有效的 Refresh Token,客戶端就會(huì)攜帶 Refresh Token 向服務(wù)器發(fā)送獲取新 Access Token 的請(qǐng)求。服務(wù)器驗(yàn)證 Refresh Token 通過(guò)后,會(huì)生成新的 Access Token 返回給客戶端,客戶端更新本地存儲(chǔ)的 Access Token,然后繼續(xù)后續(xù)操作,整個(gè)過(guò)程對(duì)用戶來(lái)說(shuō)是無(wú)感知的,極大地提升了用戶體驗(yàn),同時(shí)也保障了系統(tǒng)的安全性 。

5.2 后端實(shí)現(xiàn)步驟

  1. 在 JwtUtils 類中添加刷新令牌的方法:在之前封裝的JwtUtils類中,新增一個(gè)用于刷新令牌的方法。這個(gè)方法的作用是根據(jù)傳入的舊的 Refresh Token,生成新的 Access Token。首先從舊的 Refresh Token 中提取出用戶名,這是因?yàn)橛脩裘巧尚碌?Access Token 的關(guān)鍵信息,就像重新配鑰匙需要知道原來(lái)鑰匙對(duì)應(yīng)的鎖的相關(guān)信息一樣。然后調(diào)用之前的generateAccessToken方法,根據(jù)提取的用戶名生成新的 Access Token 并返回。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.access-token-expiration}")
    private long accessTokenExpiration;
    @Value("${jwt.refresh-token-expiration}")
    private long refreshTokenExpiration;
    // 其他方法...
    /**
     * 刷新令牌,根據(jù)Refresh Token生成新的Access Token
     * @param refreshToken 舊的Refresh Token
     * @return 新的Access Token
     */
    public String refreshToken(String refreshToken) {
        if (validateToken(refreshToken)) {
            String username = getUsernameFromToken(refreshToken);
            return generateAccessToken(username);
        }
        return null;
    }
}
  1. 在 JwtAuthenticationFilter 中添加檢查 token 是否即將過(guò)期并刷新的邏輯:在JwtAuthenticationFilter過(guò)濾器中,增加對(duì) Access Token 是否即將過(guò)期的檢查邏輯。當(dāng)服務(wù)器接收到請(qǐng)求時(shí),會(huì)先從請(qǐng)求頭中獲取 Access Token,然后判斷該 Token 是否有效且即將過(guò)期。這里設(shè)置一個(gè)閾值,比如當(dāng) Token 剩余有效期小于 5 分鐘時(shí),認(rèn)為即將過(guò)期 。如果即將過(guò)期,調(diào)用JwtUtils中的refreshToken方法,生成新的 Access Token,并將新的 Token 添加到響應(yīng)頭中返回給客戶端,這樣客戶端就能及時(shí)更新本地的 Access Token,實(shí)現(xiàn)無(wú)感刷新。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    private static final long REFRESH_THRESHOLD = 5 * 60 * 1000; // 5分鐘,即將過(guò)期的閾值,單位毫秒

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 跳過(guò)登錄和刷新令牌的接口
        String requestURI = request.getRequestURI();
        if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
            chain.doFilter(request, response);
            return;
        }

        // 從請(qǐng)求頭獲取token
        String token = getTokenFromRequest(request);

        if (token != null && jwtUtils.validateToken(token)) {
            // 從token中獲取用戶名
            String username = jwtUtils.getUsernameFromToken(token);

            // 判斷token是否即將過(guò)期
            Date expiration = jwtUtils.getClaimsFromToken(token).getExpiration();
            long remainingTime = expiration.getTime() - System.currentTimeMillis();
            if (remainingTime < REFRESH_THRESHOLD) {
                // 生成新的accessToken
                String newAccessToken = jwtUtils.refreshToken(token);
                if (newAccessToken != null) {
                    response.setHeader("Authorization", "Bearer " + newAccessToken);
                }
            }

            // 從UserDetailsService中獲取用戶詳細(xì)信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // 創(chuàng)建認(rèn)證對(duì)象
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
            );
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // 將認(rèn)證對(duì)象存入SecurityContextHolder
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        chain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

5.3 前端配合實(shí)現(xiàn)

前端在實(shí)現(xiàn)無(wú)感刷新機(jī)制中起著重要的配合作用,主要涉及以下幾個(gè)關(guān)鍵步驟:

  1. 保存 token 和過(guò)期時(shí)間:當(dāng)用戶登錄成功后,前端會(huì)從后端返回的響應(yīng)中獲取 Access Token 和 Refresh Token,并將它們存儲(chǔ)在本地。通??梢允褂?code>localStorage或者sessionStorage來(lái)存儲(chǔ)這些信息 。同時(shí),為了方便后續(xù)判斷 Token 是否過(guò)期,還需要從 Token 的 Payload 中解析出過(guò)期時(shí)間并保存。以localStorage為例,假設(shè)后端返回的響應(yīng)數(shù)據(jù)是一個(gè)包含accessTokenrefreshToken的 JSON 對(duì)象:
// 假設(shè)后端返回的數(shù)據(jù)
const responseData = {
    accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
    refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
    // 假設(shè)后端同時(shí)返回過(guò)期時(shí)間(毫秒時(shí)間戳)
    accessTokenExpiration: 1616239022000
};

// 保存token和過(guò)期時(shí)間
localStorage.setItem('accessToken', responseData.accessToken);
localStorage.setItem('refreshToken', responseData.refreshToken);
localStorage.setItem('accessTokenExpiration', responseData.accessTokenExpiration);
  1. 檢查 token 過(guò)期時(shí)間:在每次前端發(fā)送 HTTP 請(qǐng)求前,都需要檢查本地存儲(chǔ)的 Access Token 是否即將過(guò)期。通過(guò)獲取當(dāng)前時(shí)間,并與之前保存的 Access Token 過(guò)期時(shí)間進(jìn)行比較,判斷是否需要刷新 Token。這里同樣設(shè)置一個(gè)閾值,比如當(dāng)距離過(guò)期時(shí)間小于 1 分鐘時(shí),觸發(fā)刷新操作 。可以使用一個(gè)自定義的函數(shù)來(lái)實(shí)現(xiàn)這個(gè)檢查邏輯:
function checkTokenExpiration() {
    const accessToken = localStorage.getItem('accessToken');
    const accessTokenExpiration = parseInt(localStorage.getItem('accessTokenExpiration'));
    const currentTime = Date.now();

    // 設(shè)置閾值,距離過(guò)期時(shí)間小于1分鐘時(shí)觸發(fā)刷新
    const threshold = 60 * 1000; 
    if (accessToken && accessTokenExpiration && (accessTokenExpiration - currentTime) < threshold) {
        return true;
    }
    return false;
}
  1. 發(fā)送續(xù)約請(qǐng)求:當(dāng)檢查發(fā)現(xiàn) Access Token 即將過(guò)期時(shí),前端需要攜帶舊的 Refresh Token 向服務(wù)器發(fā)送續(xù)約請(qǐng)求,以獲取新的 Access Token。通常會(huì)有一個(gè)專門(mén)的后端接口用于處理這個(gè)續(xù)約請(qǐng)求,比如/api/auth/refresh 。在發(fā)送請(qǐng)求時(shí),將 Refresh Token 放在請(qǐng)求頭或者請(qǐng)求體中傳遞給后端。這里以使用axios庫(kù)發(fā)送請(qǐng)求為例:
import axios from 'axios';

async function renewToken() {
    const refreshToken = localStorage.getItem('refreshToken');
    try {
        const response = await axios.post('/api/auth/refresh', { refreshToken }, {
            headers: {
                'Content-Type': 'application/json'
            }
        });
        const newAccessToken = response.data.accessToken;
        const newAccessTokenExpiration = response.data.accessTokenExpiration;

        // 更新本地存儲(chǔ)的token和過(guò)期時(shí)間
        localStorage.setItem('accessToken', newAccessToken);
        localStorage.setItem('accessTokenExpiration', newAccessTokenExpiration);

        return newAccessToken;
    } catch (error) {
        console.error('Token續(xù)約失敗', error);
        // 處理續(xù)約失敗的情況,比如跳轉(zhuǎn)到登錄頁(yè)面
        window.location.href = '/login';
    }
}
  1. 更新本地存儲(chǔ):當(dāng)后端成功返回新的 Access Token 和相關(guān)信息(如過(guò)期時(shí)間)后,前端需要及時(shí)更新本地存儲(chǔ)的 Token 和過(guò)期時(shí)間,確保后續(xù)請(qǐng)求使用的是最新的有效令牌。在上述renewToken函數(shù)中,已經(jīng)包含了更新本地存儲(chǔ)的操作,通過(guò)localStorage.setItem方法將新的 Access Token 和過(guò)期時(shí)間重新保存 。

在實(shí)際應(yīng)用中,為了使代碼結(jié)構(gòu)更清晰、邏輯更嚴(yán)謹(jǐn),可以將上述功能封裝成一個(gè)獨(dú)立的模塊,并結(jié)合前端框架(如 Vue、React 等)的特性,將這些邏輯集成到請(qǐng)求攔截器中,實(shí)現(xiàn)對(duì)所有請(qǐng)求的統(tǒng)一處理,確保在用戶無(wú)感知的情況下完成 Token 的刷新,提升用戶體驗(yàn)和系統(tǒng)的安全性 。例如,在 Vue 項(xiàng)目中,可以利用axios的攔截器機(jī)制,在請(qǐng)求發(fā)送前自動(dòng)檢查 Token 并進(jìn)行刷新操作:

import axios from 'axios';

// 創(chuàng)建axios實(shí)例
const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API, // api的base_url
    timeout: 5000 // 請(qǐng)求超時(shí)時(shí)間
});

// 請(qǐng)求攔截器
service.interceptors.request.use(config => {
    if (checkTokenExpiration()) {
        return renewToken().then(newAccessToken => {
            config.headers['Authorization'] = 'Bearer'+ newAccessToken;
            return config;
        });
    } else {
        const accessToken = localStorage.getItem('accessToken');
        if (accessToken) {
            config.headers['Authorization'] = 'Bearer'+ accessToken;
        }
        return config;
    }
}, error => {
    console.log(error); // for debug
    Promise.reject(error);
});

export default service;

通過(guò)上述前端配合實(shí)現(xiàn)的步驟,與后端的無(wú)感刷新機(jī)制相結(jié)合,形成了一個(gè)完整的、用戶無(wú)感知的 Token 刷新流程,有效提升了應(yīng)用的用戶體驗(yàn)和安全性,確保用戶在使用應(yīng)用過(guò)程中,不會(huì)因?yàn)?Token 過(guò)期而頻繁中斷操作,需要重新登錄 。

六、安全與優(yōu)化考量

6.1 Refresh Token 安全措施

Refresh Token 作為獲取新 Access Token 的關(guān)鍵憑證,其安全性至關(guān)重要,直接關(guān)系到用戶身份的持續(xù)有效性和系統(tǒng)的整體安全性。為了切實(shí)保障 Refresh Token 的安全,我們可以采取以下多種有效措施:

  • 限制生命周期:為 Refresh Token 設(shè)置合理的有效期,避免其長(zhǎng)期有效。雖然 Refresh Token 相較于 Access Token 有效期更長(zhǎng),但如果無(wú)限期有效,一旦泄露,就會(huì)給惡意攻擊者提供長(zhǎng)時(shí)間冒用用戶身份的機(jī)會(huì)。例如,將 Refresh Token 的有效期設(shè)置為 7 天,這樣即使 Refresh Token 不幸泄露,攻擊者能利用的時(shí)間也被限制在 7 天內(nèi),大大降低了安全風(fēng)險(xiǎn)。在JwtUtils類中設(shè)置過(guò)期時(shí)間時(shí),通過(guò)@Value注解從配置文件讀取過(guò)期時(shí)間參數(shù),如@Value("${jwt.refresh-token-expiration}") private long refreshTokenExpiration;,在生成刷新令牌的方法中使用該參數(shù)return generateToken(claims, username, refreshTokenExpiration); ,確保按照配置的有效期生成刷新令牌。
  • 禁止跨域使用:通過(guò)設(shè)置 HTTP 響應(yīng)頭,如Access-Control-Allow-Origin,嚴(yán)格限制 Refresh Token 只能在同域請(qǐng)求中使用。這是因?yàn)榭缬蛘?qǐng)求容易受到跨站請(qǐng)求偽造(CSRF)等攻擊,若 Refresh Token 在跨域場(chǎng)景下被濫用,攻擊者可能會(huì)借助用戶的身份在其他域中進(jìn)行非法操作。在 Spring Boot 項(xiàng)目中,可以使用過(guò)濾器或者 Spring Security 的配置來(lái)實(shí)現(xiàn)這一限制。例如,使用 Spring Security 的配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
           .cors().configurationSource(request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Collections.singletonList("http://your-allowed-origin.com"));
                config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
                return config;
            })
           .and()
            // 其他配置...
    }
}

這樣就限制了只有http://your-allowed-origin.com這個(gè)域的請(qǐng)求才能攜帶 Refresh Token 進(jìn)行訪問(wèn),有效防止了跨域攻擊。

  • 考慮綁定設(shè)備:將 Refresh Token 與用戶設(shè)備信息進(jìn)行綁定,比如設(shè)備的唯一標(biāo)識(shí)(如 IMEI、MAC 地址等) 。這樣一來(lái),即使 Refresh Token 被泄露,由于與原設(shè)備信息不匹配,攻擊者也無(wú)法在其他設(shè)備上成功使用。實(shí)現(xiàn)設(shè)備綁定可以在生成 Refresh Token 時(shí),將設(shè)備標(biāo)識(shí)作為自定義聲明(claim)添加到 JWT 的 Payload 中。例如:
public String generateRefreshToken(String username, String deviceId) {
    Map<String, Object> claims = new HashMap<>();
    claims.put("deviceId", deviceId);
    return generateToken(claims, username, refreshTokenExpiration);
}

在驗(yàn)證 Refresh Token 時(shí),從 Token 的 Payload 中提取設(shè)備標(biāo)識(shí),并與當(dāng)前請(qǐng)求設(shè)備的標(biāo)識(shí)進(jìn)行比對(duì),若不一致則拒絕請(qǐng)求,從而增強(qiáng)了 Refresh Token 的安全性。

6.2 離線刷新策略

離線刷新 Token,簡(jiǎn)單來(lái)說(shuō),就是在客戶端檢測(cè)到 Token 即將過(guò)期時(shí),即便此時(shí)沒(méi)有新的請(qǐng)求發(fā)生,客戶端也會(huì)主動(dòng)向服務(wù)器發(fā)起請(qǐng)求,獲取新的 Token,以確保用戶在下次使用應(yīng)用時(shí),Token 仍然有效,整個(gè)過(guò)程無(wú)需用戶手動(dòng)干預(yù),真正實(shí)現(xiàn)了無(wú)感知的 Token 更新。

在實(shí)際應(yīng)用中,離線刷新策略具有重要的意義和廣泛的應(yīng)用場(chǎng)景。比如在一些需要長(zhǎng)時(shí)間運(yùn)行的應(yīng)用程序中,如在線文檔編輯工具,用戶可能會(huì)長(zhǎng)時(shí)間打開(kāi)文檔進(jìn)行編輯,期間并沒(méi)有頻繁的網(wǎng)絡(luò)請(qǐng)求。但隨著時(shí)間的推移,Token 可能會(huì)過(guò)期,如果沒(méi)有離線刷新策略,當(dāng)用戶完成編輯想要保存文檔時(shí),由于 Token 過(guò)期,保存操作就會(huì)失敗,用戶需要重新登錄,這無(wú)疑會(huì)給用戶帶來(lái)極大的困擾,影響用戶體驗(yàn)。

實(shí)現(xiàn)離線刷新策略,在前端可以利用定時(shí)器機(jī)制,定時(shí)檢查本地存儲(chǔ)的 Token 過(guò)期時(shí)間。例如,使用setInterval函數(shù),每隔一段時(shí)間(如 10 分鐘)檢查一次 Token 是否即將過(guò)期。當(dāng)檢測(cè)到 Token 即將過(guò)期(如距離過(guò)期時(shí)間小于 1 分鐘)時(shí),觸發(fā)刷新操作。以 JavaScript 代碼為例:

// 假設(shè)已經(jīng)定義了checkTokenExpiration和renewToken函數(shù)
const checkInterval = setInterval(() => {
    if (checkTokenExpiration()) {
        renewToken().then(() => {
            console.log('Token已成功刷新');
        }).catch((error) => {
            console.error('Token刷新失敗', error);
            // 處理刷新失敗的情況,如跳轉(zhuǎn)到登錄頁(yè)面
            window.location.href = '/login';
        });
    }
}, 10 * 60 * 1000); // 每隔10分鐘檢查一次

在后端,需要提供相應(yīng)的接口來(lái)處理前端發(fā)送的刷新請(qǐng)求。這個(gè)接口與之前實(shí)現(xiàn)的刷新接口類似,接收前端傳遞的 Refresh Token,驗(yàn)證其有效性后,生成新的 Access Token 并返回給前端。例如,在 Spring Boot 中,可以定義如下 Controller 方法:

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private JwtUtils jwtUtils;

    @PostMapping("/offline-refresh")
    public ResponseEntity<String> offlineRefreshToken(@RequestBody String refreshToken) {
        if (jwtUtils.validateToken(refreshToken)) {
            String newAccessToken = jwtUtils.refreshToken(refreshToken);
            if (newAccessToken != null) {
                return ResponseEntity.ok(newAccessToken);
            }
        }
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
    }
}

通過(guò)前端和后端的協(xié)同工作,實(shí)現(xiàn)了離線刷新 Token 的功能,有效提升了應(yīng)用的穩(wěn)定性和用戶體驗(yàn),確保用戶在使用應(yīng)用過(guò)程中不會(huì)因?yàn)?Token 過(guò)期而中斷操作 。

6.3 API Gateway 集成優(yōu)勢(shì)

在微服務(wù)架構(gòu)中,API Gateway 作為整個(gè)系統(tǒng)的統(tǒng)一入口,扮演著至關(guān)重要的角色。將 Token 刷新功能集成到 API Gateway 中,具有諸多顯著的優(yōu)勢(shì),能夠極大地提升系統(tǒng)的性能和靈活性。

首先,API Gateway 可以集中處理 Token 的刷新邏輯,減輕各個(gè)微服務(wù)的負(fù)擔(dān)。在傳統(tǒng)的架構(gòu)中,每個(gè)微服務(wù)都需要自行處理 Token 的驗(yàn)證和刷新,這無(wú)疑會(huì)導(dǎo)致代碼的重復(fù)編寫(xiě),增加開(kāi)發(fā)和維護(hù)的成本。而通過(guò) API Gateway 統(tǒng)一處理 Token 刷新,各個(gè)微服務(wù)只需專注于自身的業(yè)務(wù)邏輯,無(wú)需再關(guān)心復(fù)雜的認(rèn)證和授權(quán)流程,就像將繁瑣的安保工作統(tǒng)一交給專業(yè)的安保公司,各個(gè)部門(mén)就能更專注于自己的核心業(yè)務(wù)。

其次,API Gateway 能夠?qū)崿F(xiàn)更靈活的刷新策略。它可以根據(jù)不同的業(yè)務(wù)需求和場(chǎng)景,制定個(gè)性化的 Token 刷新規(guī)則。比如,對(duì)于一些對(duì)安全性要求極高的業(yè)務(wù)請(qǐng)求,可以設(shè)置更短的 Token 有效期和更頻繁的刷新機(jī)制;而對(duì)于一些普通的業(yè)務(wù)請(qǐng)求,則可以適當(dāng)放寬刷新條件,減少不必要的刷新操作,提高系統(tǒng)的性能和響應(yīng)速度。這種差異化的刷新策略,能夠更好地滿足多樣化的業(yè)務(wù)需求,提升系統(tǒng)的整體適應(yīng)性。

以 Spring Cloud Gateway 為例,實(shí)現(xiàn) Token 刷新功能的集成。首先,在 Spring Cloud Gateway 的配置文件中,定義全局過(guò)濾器,用于攔截所有的請(qǐng)求,并對(duì) Token 進(jìn)行驗(yàn)證和刷新處理。例如:

@Configuration
public class GatewayConfig {

    @Autowired
    private JwtUtils jwtUtils;

    @Bean
    public GlobalFilter jwtFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String token = request.getHeaders().getFirst("Authorization");
            if (token != null && token.startsWith("Bearer ")) {
                token = token.substring(7);
                if (jwtUtils.validateToken(token)) {
                    // 判斷Token是否即將過(guò)期
                    boolean isTokenAboutToExpire = jwtUtils.isTokenAboutToExpire(token);
                    if (isTokenAboutToExpire) {
                        // 嘗試刷新Token
                        String newToken = jwtUtils.refreshToken(token);
                        if (newToken != null) {
                            ServerHttpRequest newRequest = request.mutate()
                                   .headers(httpHeaders -> httpHeaders.set("Authorization", "Bearer " + newToken))
                                   .build();
                            return chain.filter(exchange.mutate().request(newRequest).build());
                        }
                    }
                }
            }
            return chain.filter(exchange);
        };
    }
}

在上述代碼中,通過(guò)定義jwtFilter全局過(guò)濾器,對(duì)每個(gè)進(jìn)入系統(tǒng)的請(qǐng)求進(jìn)行攔截。首先從請(qǐng)求頭中提取 Token,驗(yàn)證其有效性。如果 Token 有效且即將過(guò)期,調(diào)用JwtUtils中的refreshToken方法嘗試刷新 Token。若刷新成功,將新的 Token 添加到請(qǐng)求頭中,繼續(xù)處理請(qǐng)求;若刷新失敗或 Token 無(wú)效,則按照正常流程繼續(xù)處理請(qǐng)求,由后續(xù)的業(yè)務(wù)邏輯來(lái)決定是否拒絕訪問(wèn) 。

通過(guò)將 Token 刷新功能集成到 API Gateway 中,不僅減輕了微服務(wù)的壓力,提高了系統(tǒng)的可維護(hù)性,還實(shí)現(xiàn)了更靈活、高效的 Token 管理策略,為微服務(wù)架構(gòu)的穩(wěn)定運(yùn)行和業(yè)務(wù)的順利開(kāi)展提供了有力保障 。

以上就是SpringBoot中實(shí)現(xiàn)JWT登錄授權(quán)+無(wú)感刷新全流程的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot JWT登錄授權(quán)和無(wú)感刷新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java實(shí)現(xiàn)輸入流轉(zhuǎn)化為String

    Java實(shí)現(xiàn)輸入流轉(zhuǎn)化為String

    這篇文章主要介紹了Java實(shí)現(xiàn)輸入流轉(zhuǎn)化為String的相關(guān)資料,需要的朋友可以參考下
    2016-12-12
  • IntelliJ IDEA下Maven創(chuàng)建Scala項(xiàng)目的方法步驟

    IntelliJ IDEA下Maven創(chuàng)建Scala項(xiàng)目的方法步驟

    這篇文章主要介紹了IntelliJ IDEA下Maven創(chuàng)建Scala項(xiàng)目的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-06-06
  • SpringBoot中定時(shí)任務(wù)@Scheduled的多線程使用詳解

    SpringBoot中定時(shí)任務(wù)@Scheduled的多線程使用詳解

    這篇文章主要為大家詳細(xì)介紹了pring Boot定時(shí)任務(wù)@Scheduled的多線程原理以及如何加入線程池來(lái)處理定時(shí)任務(wù),感興趣的可以了解一下
    2023-04-04
  • springboot創(chuàng)建的web項(xiàng)目整合Quartz框架的項(xiàng)目實(shí)踐

    springboot創(chuàng)建的web項(xiàng)目整合Quartz框架的項(xiàng)目實(shí)踐

    本文主要介紹了springboot創(chuàng)建的web項(xiàng)目整合Quartz框架的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Java實(shí)現(xiàn)獲取cpu、內(nèi)存、硬盤(pán)、網(wǎng)絡(luò)等信息的方法示例

    Java實(shí)現(xiàn)獲取cpu、內(nèi)存、硬盤(pán)、網(wǎng)絡(luò)等信息的方法示例

    這篇文章主要介紹了Java實(shí)現(xiàn)獲取cpu、內(nèi)存、硬盤(pán)、網(wǎng)絡(luò)等信息的方法,涉及java使用第三方j(luò)ar包針對(duì)本機(jī)硬件的cpu、內(nèi)存、硬盤(pán)、網(wǎng)絡(luò)信息等的讀取相關(guān)操作技巧,需要的朋友可以參考下
    2018-06-06
  • idea創(chuàng)建項(xiàng)目沒(méi)有webapp文件夾的解決方法

    idea創(chuàng)建項(xiàng)目沒(méi)有webapp文件夾的解決方法

    本文主要介紹了idea創(chuàng)建項(xiàng)目沒(méi)有webapp文件夾的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-05-05
  • JVM雙親委派模型知識(shí)詳細(xì)總結(jié)

    JVM雙親委派模型知識(shí)詳細(xì)總結(jié)

    今天帶各位小伙伴學(xué)習(xí)Java虛擬機(jī)的相關(guān)知識(shí),文中對(duì)JVM雙親委派模型作了非常詳細(xì)的介紹,對(duì)正在學(xué)習(xí)java的小伙伴們有很好的幫助,需要的朋友可以參考下
    2021-05-05
  • JavaWeb Spring依賴注入深入學(xué)習(xí)

    JavaWeb Spring依賴注入深入學(xué)習(xí)

    這篇文章主要為大家詳細(xì)介紹了JavaWeb Spring依賴注入,深入學(xué)習(xí)Spring依賴注入,感興趣的小伙伴們可以參考一下
    2016-09-09
  • Java雪花算法生成分布式id詳解

    Java雪花算法生成分布式id詳解

    這篇文章主要介紹了Java雪花算法生成分布式id詳解,隨著業(yè)務(wù)的增長(zhǎng),有些表可能要占用很大的物理存儲(chǔ)空間,為了解決該問(wèn)題,后期使用數(shù)據(jù)庫(kù)分片技術(shù),將一個(gè)數(shù)據(jù)庫(kù)進(jìn)行拆分,通過(guò)數(shù)據(jù)庫(kù)中間件連接,需要的朋友可以參考下
    2024-01-01
  • 一篇文章帶你入門(mén)Java變量

    一篇文章帶你入門(mén)Java變量

    這篇文章主要介紹了Java變量,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-08-08

最新評(píng)論

永宁县| 杭锦旗| 衡山县| 隆回县| 行唐县| 句容市| 行唐县| 赫章县| 云南省| 南部县| 和硕县| 德州市| 调兵山市| 龙山县| 永年县| 台北市| 恩平市| 梅河口市| 宁河县| 衡山县| 华蓥市| 武威市| 平江县| 紫云| 靖江市| 舞钢市| 吐鲁番市| 富源县| 阿鲁科尔沁旗| 闽侯县| 威海市| 历史| 九江县| 宝兴县| 琼结县| 克什克腾旗| 博乐市| 合阳县| 府谷县| 璧山县| 长乐市|