【SpringBoot】最佳實踐——JWT結合Redis實現雙Token無感刷新

JWT概覽

JWT概念

JWT是全稱是JSON WEB TOKEN,是一個開放標準,用于將各方數據信息作為JSON格式進行對象傳遞,可以對數據進行可選的數字加密,可使用RSAECDSA進行公鑰/私鑰簽名。JWT最常見的使用場景就是緩存當前用戶登錄信息,當用戶登錄成功之后,拿到JWT,之后用戶的每一個請求在請求頭攜帶上Authorization字段來辨別區分請求的用戶信息。且不需要額外的資源開銷。

JWT組成部分

JWT通常由一個頭部(Header)、一個負載(Payload)和一個簽名(Signature)三部分組成,這三部分之間用點(.)分隔。所以,一個完整的JWT看起來像這樣:

xxxxx.yyyyy.zzzzz

下面我們來詳細解析每一部分:

頭部(header)

頭部用于描述令牌的元數據,通常包含令牌的類型(即JWT)和所使用的簽名算法(如HMAC SHA256)。

  • typ:表示令牌的類型,JWT令牌統一寫為"JWT"。
  • alg:表示簽名使用的算法,例如HMAC SHA256或RSA。

頭部信息會被進行Base64編碼,形成JWT的第一部分。

{  "typ": "JWT",  "alg": "HS256"  
}

負載(payload)

負載包含了JWT的聲明,即傳遞的數據,這些數據通常包括用戶信息和其他相關數據。聲明有三種類型:注冊的聲明、公共的聲明和私有的聲明。

  • 注冊的聲明:這是一組預定義的聲明,它們不是強制的,但是推薦使用,以提供一組有用的、可互操作的聲明。如:iat(簽發時間)、exp(過期時間)、aud(接收方)、sub(用戶唯一標識)、jti(JWT唯一標識)等。
  • 公共的聲明:可以定義任何名稱,但應避免與注冊的聲明名稱沖突。
  • 私有的聲明:是提供者和消費者之間共同定義的聲明。

負載同樣會被Base64編碼,形成JWT的第二部分。

{  "sub": "1234567890",  "name": "John Doe",  "jti": "unique-jwt-id","admin": true  
}

簽名(signature)

簽名將頭部和負載用指定的算法進行簽名,驗證JWT的真實性和完整性。當接收者收到JWT時,他們可以使用相同的算法和密鑰(對于HMAC算法)或使用公鑰(對于RSA或ECDSA算法)驗證簽名。如果兩個簽名匹配,那么JWT就是有效的。

簽名的過程如下:

  • 先將Base64編碼后的頭部和負載數據用點號(.)連接起來。
  • 使用指定的簽名算法(例如,HMAC SHA256、RSA、ECDSA)和密鑰對連接后的字符串進行簽名。
  • 將生成的簽名部分進行Base64Url編碼,形成JWT的第三部分。

簽名部分也是經過Base64Url編碼的,形成JWT的第三部分。

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

注意:雖然Base64Url編碼不是加密方式,但它可以確保JWT的字符串格式是緊湊的,并且容易在URL、POST參數或HTTP頭部中傳輸。

技術方案設計

單點登錄(SSO)

  • 單點登錄(Single Sign-On, SSO) 是一種身份認證機制,允許用戶通過一次登錄即可訪問多個相互信任的應用系統,而無需重復輸入認證信息。

雙Token機制

  • AccessToken
    • 短期有效(如30分鐘),用于接口訪問。
    • 客戶端每次請求API時攜帶。
    • 不持久化存儲,僅通過簽名驗證合法性。
  • RefreshToken
    • 用于獲取新的Access Token,有效期長(如3天)。
    • 僅在刷新令牌時傳輸,不直接訪問業務API。
    • 必須持久化存儲(如Redis),服務端可主動使其失效。
  • 簽名算法:使用RSA非對稱加密算法,減少內存占用,防止篡改,并方便后續拓展子系統。

無感刷新Token

  • 客戶端將由于AccesssToken過期失敗的請求存儲起來,攜帶RefreshToken成功刷新Token后,將存儲的失敗請求重新發起,以此達到用戶無感的體驗。
  • 服務端根據RefreshToken解析出userId和deviceId后,去Redis中查詢存儲的RefreshToken并進行比對,成功后生成新的AT和RT并返回

多端會話管理

  • 同一賬號在不同設備登錄時,為每個設備生成獨立的RefreshToken。
  • Redis中以 userId:deviceId為鍵存儲RefreshToken,過期時間設置為RefreshToken的過期時間。

廢棄令牌移除

  • Redis中以 blacklist:token 為鍵存儲AccessToken黑名單,鍵值對的過期時間設置為AccessToken的剩余有效期。
  • 直接刪除Redis中的RefreshToken。

最佳實踐

總體流程

JWT工具類

// JWT工具類
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.access.expiration}")private long accessExpiration;@Value("${jwt.refresh.expiration}")private long refreshExpiration;public String generateAccessToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + accessExpiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}public String generateRefreshToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + refreshExpiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}public boolean validateToken(String token) {try {Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true;} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {return false;}}public String getUsernameFromToken(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();}
}

Redis服務類

// Redis服務類
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;@Service
public class RedisService {private final StringRedisTemplate redisTemplate;public RedisService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}public void saveRefreshToken(String refreshToken, String username) {redisTemplate.opsForValue().set("refresh_token:" + refreshToken, username, 7, TimeUnit.DAYS);}public boolean isRefreshTokenValid(String refreshToken) {return redisTemplate.hasKey("refresh_token:" + refreshToken);}public void deleteRefreshToken(String refreshToken) {redisTemplate.delete("refresh_token:" + refreshToken);}public void addToBlacklist(String accessToken, long expirationMs) {redisTemplate.opsForValue().set("blacklist:" + accessToken, "invalid", expirationMs, TimeUnit.MILLISECONDS);}public boolean isInBlacklist(String accessToken) {return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + accessToken));}
}

Filter過濾器

// JWT過濾器
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
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;public class JwtFilter extends OncePerRequestFilter {private final JwtUtil jwtUtil;private final RedisService redisService;public JwtFilter(JwtUtil jwtUtil, RedisService redisService) {this.jwtUtil = jwtUtil;this.redisService = redisService;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = resolveToken(request);if (token == null) {filterChain.doFilter(request, response);return;}if (redisService.isInBlacklist(token)) {sendError(response, "Token invalid");return;}if (jwtUtil.validateToken(token)) {String username = jwtUtil.getUsernameFromToken(token);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null);SecurityContextHolder.getContext().setAuthentication(authentication);filterChain.doFilter(request, response);} else {sendError(response, "Token expired or invalid");}}private String resolveToken(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (bearerToken != null && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}private void sendError(HttpServletResponse response, String message) throws IOException {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write(message);response.getWriter().flush();}
}

Controller類

// 控制器類
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;@RestController
public class AuthController {private final JwtUtil jwtUtil;private final RedisService redisService;public AuthController(JwtUtil jwtUtil, RedisService redisService) {this.jwtUtil = jwtUtil;this.redisService = redisService;}@PostMapping("/login")public ResponseEntity<?> login(@RequestBody LoginRequest request) {// 這里應添加用戶認證邏輯(如數據庫驗證)String username = request.getUsername();String accessToken = jwtUtil.generateAccessToken(username);String refreshToken = jwtUtil.generateRefreshToken(username);redisService.saveRefreshToken(refreshToken, username);return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken));}@PostMapping("/refresh")public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest request) {String refreshToken = request.getRefreshToken();if (!redisService.isRefreshTokenValid(refreshToken)) {return ResponseEntity.status(401).body("Invalid refresh token");}String username = jwtUtil.getUsernameFromToken(refreshToken);String newAccessToken = jwtUtil.generateAccessToken(username);String newRefreshToken = jwtUtil.generateRefreshToken(username);// 替換舊refreshTokenredisService.deleteRefreshToken(refreshToken);redisService.saveRefreshToken(newRefreshToken, username);// 將舊accessToken加入黑名單(可選)// long expiration = jwtUtil.getExpirationFromToken(refreshToken).getTime() - System.currentTimeMillis();// redisService.addToBlacklist(refreshToken, expiration);return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken));}// DTO類private static class LoginRequest {private String username;private String password;// getters/setters}private static class RefreshRequest {private String refreshToken;// getters/setters}private static class TokenResponse {private final String accessToken;private final String refreshToken;// constructor/getters}
}

問題解析

相比單Token的優勢

  • 高安全性:用戶請求僅攜帶過期時間較短的AccessToken,即使令牌泄露,風險時間窗口也較小;用戶僅在請求刷新Token時攜帶RefreshToken
  • 長會話:RefreshToken一般設置較長的過期時間,只要RT不過期用戶就無需重復登錄

引入Redis的作用

  • 方便狀態管理:如果不存在Redis,用戶登出后只能等待Token過期才能被動失效,增加Token暴露風險;通過在Redis中引入黑名單blacklist,可以使得Token主動失效
  • 多端會話管理:通過以 userId:deviceId為鍵存儲不同設備的Token,實現同用戶多端登錄。通過刪除對應設備的鍵并加上黑名單,可以主動剔出對應設備
  • 分布式一致性:若使用本地內存存儲 RT,在分布式多節點架構中,各節點無法共享 RT 狀態,導致用戶在一個節點退出后,其他節點仍認為 RT 有效。Redis作為集中式存儲,確保所有服務節點訪問同一份 RT 數據,狀態一致。

保證Token安全性

  • 存儲安全性:AT存于內存或 SessionStorage(頁面關閉失效),而RT通過 HttpOnly; Secure; SameSite=Strict Cookie 存儲(XSS攻擊無效)。
  • 傳輸安全性:開啟HTTPS,防止中間人攻擊(篡改、偽造和竊聽);AT通過 Authorization: Bearer {token} 請求頭傳遞,避免 URL 參數(防日志泄露),而RT通過 Cookie(標記 HttpOnly; Secure)傳輸。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/73477.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/73477.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/73477.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

面試系列|螞蟻金服技術面【1】

哈嘍&#xff0c;大家好&#xff01;今天分享一下螞蟻金服的 Java 后端開發崗位真實社招面經&#xff0c;復盤面試過程中踩過的坑&#xff0c;整理面試過程中提到的知識點&#xff0c;希望能給正在準備面試的你一些參考和啟發&#xff0c;希望對你有幫助&#xff0c;愿你能夠獲…

eBPF 實時捕獲鍵盤輸入

eBPF 實時捕獲鍵盤輸入 本文將帶你一步步實現一個基于eBPF kprobe的鍵盤記錄功能&#xff0c;通過Go語言配合libbpfgo&#xff0c;你將學會如何無損地監控系統鍵盤輸入&#xff0c;并從中獲取實時數據&#xff0c;進一步提高系統安全和監控能力。 1. 說明 本文屬于專欄 Go語言…

APB-清華聯合騰訊等機構推出的分布式長上下文推理框架

APB (Accelerating Distributed Long-Context Inference by Passing Compressed Context Blocks acrossGPUs)是清華大學等機構聯合提出的分布式長上下文推理框架。通過稀疏注意力機制和序列并行推理方式&#xff0c;有效解決了大模型處理長文本時的效率瓶頸。APB采用更小的Anch…

數據庫分庫分表介紹

分庫分表是解決數據庫性能瓶頸的常用技術手段&#xff0c;主要用于應對數據量過大、讀寫壓力過高的問題。通過將數據分散到多個數據庫或表中&#xff0c;可以提高系統的擴展性和性能。 1. 分庫分表的核心概念 &#xff08;1&#xff09;分庫 定義&#xff1a;將數據分散到多個…

#mapreduce打包#maven:could not resolve dependencies for project

打包報錯&#xff1a; #報錯信息&#xff1a; [ERROR] Failed to execute goal on project mapreduce_teacher1: Could not resolve dependencies for project org.example:mapreduce_teacher1:jar:1.0-SNAPSHOT: Failed to collect dependencies at org.apache.hive:hive-exe…

Rabit

之前發過rabit了&#xff0c;所以這里不再贅述&#xff0c;講講原理 在線Rabbit加密 | Rabbit解密- 在線工具 (sojson.com) rabbit加密原理 Rabbit加密算法是一種流密碼算法&#xff0c;由Daniel J. Bernstein設計&#xff0c;并被廣泛用于多種加密和安全通信應用中。它的設…

【A2DP】深入解讀A2DP中通用訪問配置文件(GAP)的互操作性要求

目錄 一、模式支持要求 1.1 發現模式 1.2 連接模式 1.3 綁定模式 1.4 模式間依賴關系總結 1.5 注意事項 1.6 協議設計深層邏輯 二、安全機制&#xff08;Security Aspects&#xff09; 三、空閑模式操作&#xff08;Idle Mode Procedures&#xff09; 3.1 支持要求 …

模型蒸餾系列——開源項目

推薦項目&#xff1a;MiniMind&#xff08;低成本全流程訓練框架&#xff09; GitHub&#xff1a;https://github.com/jingyaogong/minimind 核心特性&#xff1a;完整實現從數據清洗到模型部署的全流程&#xff0c;支持單卡低成本訓練&#xff0c;代碼全透明&#xff0c;適合…

【軟考-架構】13.1、軟件架構概述-構件技術

?資料&文章更新? GitHub地址&#xff1a;https://github.com/tyronczt/system_architect 文章目錄 ?【重點】系統架構設計軟件架構概述軟件架構設計與生命周期構件&#x1f31f;軟件架構風格數據流風格調用/返回風格獨立構件風格虛擬機風格倉庫風格閉環控制風格C2體系結…

《Android啟動偵探團:追蹤Launcher啟動的“最后一公里”》

1. 開機儀式的“黑屏懸案” 當Android設備完成開機動畫后&#xff0c;某些產品會陷入詭異的“黑屏時刻”——仿佛系統在玩捉迷藏。此時&#xff0c;**Launcher&#xff08;桌面&#xff09;**就是躲貓貓的主角。我們的任務&#xff1a;揪出Launcher何時完成啟動&#xff0c;終…

Redis事務與管道

Redis事務 可以一次執行多個命令&#xff0c;本質是一組命令的集合。一個事務中的所有命令都會序列化&#xff0c;按順序地串行執行而不會被其他命令插入&#xff0c;不許加塞。 一個隊列中&#xff0c;一次性、順序性、排他性的執行一系列命令。 Redis事務VS數據庫事務 常用…

掌握這些 UI 交互設計原則,提升產品易用性

在當今數字化時代&#xff0c;用戶對于產品的體驗要求越來越高&#xff0c;UI 交互設計成為決定產品成敗的關鍵因素之一。一個易用的產品能夠讓用戶輕松、高效地完成各種操作&#xff0c;而實現這一目標的核心在于遵循一系列科學合理的 UI 交互設計原則。本文將詳細闡述簡潔性、…

Alembic 實戰指南:快速入門到FastAPI 集成

一、快速開始 1.1 簡介 Alembic 是一個基于 SQLAlchemy 的數據庫遷移工具&#xff0c;主要用于管理數據庫模式&#xff08;Schema&#xff09;的變更&#xff0c;例如新增表、修改字段、刪除索引等&#xff0c;確保數據庫結構與應用程序的 ORM 模型保持一致。 Alembic 通過版…

LRU(最近最少使用)算法實現

核心思想與基本思路 LRU&#xff08;Least Recently Used&#xff09;算法是一種緩存淘汰策略&#xff0c;其核心思想是淘汰最近最少使用的數據。 最近使用原則&#xff1a;最近被訪問的數據在未來被訪問的概率更高&#xff0c;因此應保留在緩存中。淘汰機制&#xff1a;當緩…

現在有分段、句子數量可能不一致的中英文文本,如何用python實現中英文對照翻譯(即每行英文對應相應的中文)

以下是處理分段且中英文句子數量可能不一致的文本的Python實現方案&#xff0c;包含分句、翻譯和對齊功能&#xff1a; from googletrans import Translator import redef split_paragraphs(text):"""按空行分割段落并清洗"""return [p.strip()…

C語言每日一練——day_8

引言 針對初學者&#xff0c;每日練習幾個題&#xff0c;快速上手C語言。第八天。&#xff08;連續更新中&#xff09; 采用在線OJ的形式 什么是在線OJ&#xff1f; 在線判題系統&#xff08;英語&#xff1a;Online Judge&#xff0c;縮寫OJ&#xff09;是一種在編程競賽中用…

基礎知識《Redis解析》

Redis 詳細解析與介紹 Redis&#xff08;Remote Dictionary Server&#xff09;是一個開源的高性能鍵值對&#xff08;Key-Value&#xff09;數據庫&#xff0c;支持多種數據結構&#xff08;如字符串、哈希、列表、集合等&#xff09;&#xff0c;廣泛應用于緩存、消息隊列、…

區跨鏈知識和概念

1、以太坊 Geth 源碼解析 Geth&#xff08;Go Ethereum&#xff09;是以太坊官方提供的 Go 語言實現的客戶端&#xff0c;廣泛用于以太坊全節點運行、挖礦、DApp 開發等。理解 Geth 的源碼有助于掌握以太坊區塊鏈底層邏輯&#xff0c;如區塊同步、EVM 執行、P2P 交互等。 2、…

Vue 計算屬性與 Data 屬性同名問題深度解析

文章目錄 1. 問題背景與核心概念1.1 Vue 響應式系統架構1.2 核心概念定義 2. 同名問題的技術分析2.1 同名場景示例2.2 問題發生機制 3. 底層原理剖析3.1 Vue 初始化流程3.2 響應式系統關鍵代碼 4. 問題解決方案4.1 最佳實踐建議4.2 錯誤處理機制 5. 性能影響分析5.1 遞歸調用性…

Mybatis——基礎操作、動態SQL

目錄 一.基礎操作 1.刪除 2.新增 3.更新 4.查詢 5.XML映射文件 二、動態SQL 1.<if> 2.<where> 3.<set> 4.<foreach> 5.<sql> 6.<include> 一.基礎操作 1.刪除 參數占位符&#xff1a; 注意&#xff1a; #{...}相比于${...}…