Spring Security JWT 鑒權鏈路完整解析
一次完整的 Spring Security JWT 鑒權鏈路解析
在實際項目中,我們經(jīng)常會在 Controller 里寫出這樣一個方法簽名:
@GetMapping("/me")
public AuthUserResponse me(@AuthenticationPrincipal Jwt jwt) {
long userId = jwtService.extractUserId(jwt);
return authService.me(userId);
}
看起來 Spring 能“憑空”把一個 Jwt 對象塞進方法參數(shù)里,還自動從請求頭里的 Authorization: Bearer 提取 Token 并完成校驗。
這篇文章就結合項目,從配置到代碼,串起這條鑒權鏈路的每一步。
一、整體流程總覽
從前端調(diào)用 /api/v1/auth/me 開始,到方法參數(shù)里拿到 Jwt jwt,整個鏈路可以概括為:
- 前端發(fā)請求:HTTP 請求頭里帶
Authorization: Bearer <accessToken>。 - Security 過濾器攔截:
SecurityFilterChain中的 Bearer Token 過濾器從請求頭里截出<accessToken>。 - 交給
JwtDecoder校驗解析:過濾器調(diào)用配置好的JwtDecoder(基于 RSA 公鑰的NimbusJwtDecoder),做簽名、過期等校驗,解析出 Claim,得到一個Jwt對象。 - 封裝
Authentication放入SecurityContext:框架把Jwt封裝成JwtAuthenticationToken,寫入當前線程的SecurityContext。 - Controller 使用
@AuthenticationPrincipal Jwt注入當前用戶 Token:@AuthenticationPrincipal從SecurityContext里取出 principal(類型為Jwt)注入到方法參數(shù)。 - 業(yè)務層解析用戶 ID:
jwtService.extractUserId(jwt)從 Claim 中讀取uid,實現(xiàn)當前登錄用戶識別與后續(xù)業(yè)務處理。
下面按照“配置層 → 過濾器層 → 控制器層”的順序,一步步展開。
二、開啟資源服務器模式:SecurityFilterChain的核心配置
項目中 Spring Security 的入口配置在 SecurityConfig 中:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
// 公開內(nèi)容:首頁 Feed 不需要登錄
.requestMatchers("/api/v1/knowposts/feed").permitAll()
// 知文詳情等其他白名單接口...
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}這里有幾個關鍵點:
- 無狀態(tài)會話:
SessionCreationPolicy.STATELESS- 服務端不使用 Session 記住“誰登錄過”,每個請求都必須自帶 Token。
- 訪問控制:
- 白名單接口使用
permitAll()直接放行; - 其他接口通過
.anyRequest().authenticated()強制要求認證。
- 白名單接口使用
- 啟用 JWT 資源服務器模式:
oauth2ResourceServer(oauth -> oauth.jwt())- 告訴 Spring Security 本應用是一個 OAuth2 Resource Server;
- 請求需要通過 Bearer Token + JWT 的方式來進行認證;
- 框架自動往過濾器鏈里加上 Bearer Token 相關的過濾器和認證器。
這樣一來,我們就不需要自己在每個接口里手動解析請求頭,整個 Token 解析與校驗流程都由 Spring Security 統(tǒng)一接管。
三、JWT 編解碼 Bean:JwtEncoder與JwtDecoder
要讓資源服務器真正“識別”JWT,我們必須告訴它如何簽發(fā)和校驗 Token,這部分邏輯集中在 AuthConfiguration 中:
@Configuration
@EnableConfigurationProperties(AuthProperties.class)
@RequiredArgsConstructor
public class AuthConfiguration {
private final AuthProperties properties;
@Bean
public JwtEncoder jwtEncoder() {
AuthProperties.Jwt jwtProps = properties.getJwt();
RSAPrivateKey privateKey = PemUtils.readPrivateKey(jwtProps.getPrivateKey());
RSAPublicKey publicKey = PemUtils.readPublicKey(jwtProps.getPublicKey());
RSAKey jwk = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(jwtProps.getKeyId())
.build();
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder() {
AuthProperties.Jwt jwtProps = properties.getJwt();
RSAPublicKey publicKey = PemUtils.readPublicKey(jwtProps.getPublicKey());
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
}JwtEncoder- 讀取配置中的 RSA 私鑰、公鑰與 keyId;
- 構造基于 Nimbus 的
JwtEncoder; - 用于登錄/注冊成功后簽發(fā) Access Token 與 Refresh Token。
JwtDecoder- 只讀取 RSA 公鑰;
- 創(chuàng)建
NimbusJwtDecoder,用于校驗簽名、過期時間等信息并解析 Claim。
關鍵點:
一旦容器中存在一個 JwtDecoder Bean,oauth2ResourceServer().jwt() 會自動使用它來完成 JWT 的解析與驗證,無需手動再把 JwtDecoder 綁定到過濾器上。
四、自定義 JWT 服務:簽發(fā)與解析的業(yè)務封裝JwtService
在業(yè)務層,我們通過 JwtService 把 Token 的簽發(fā)與解析從 Controller 和 Service 中抽離出來:
@Service
@RequiredArgsConstructor
public class JwtService {
private static final String CLAIM_TOKEN_TYPE = "token_type";
private static final String CLAIM_USER_ID = "uid";
private final JwtEncoder jwtEncoder;
private final JwtDecoder jwtDecoder;
private final AuthProperties properties;
private final Clock clock = Clock.systemUTC();
public TokenPair issueTokenPair(User user) {
String refreshTokenId = UUID.randomUUID().toString();
Instant issuedAt = Instant.now(clock);
Instant accessExpiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl());
Instant refreshExpiresAt = issuedAt.plus(properties.getJwt().getRefreshTokenTtl());
String accessToken = encodeToken(user, issuedAt, accessExpiresAt, "access", UUID.randomUUID().toString());
String refreshToken = encodeRefreshToken(user, issuedAt, refreshExpiresAt, refreshTokenId);
return new TokenPair(accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, refreshTokenId);
}
public Jwt decode(String token) {
return jwtDecoder.decode(token);
}
public long extractUserId(Jwt jwt) {
Object claim = jwt.getClaims().get(CLAIM_USER_ID);
if (claim instanceof Number number) {
return number.longValue();
}
if (claim instanceof String text) {
return Long.parseLong(text);
}
throw new IllegalArgumentException("Invalid user id in token");
}
}在這里:
- 簽發(fā)階段:
- 使用
JwtEncoder構造帶有uid、token_type、jti等 Claim 的 Token; - Access Token 與 Refresh Token 使用不同的 TTL 與類型標記。
- 使用
- 解析階段:
- 可以通過
decode()手動解析某個 Token(例如刷新或登出時用); - 對于
/me等需要“當前用戶”的接口,更多使用extractUserId(jwt),從已經(jīng)被框架解析過的Jwt中讀取uidClaim。
- 可以通過
五、請求進入時:Filter 如何一步步完成 JWT 鑒權
當一個請求攜帶:
GET /api/v1/auth/meAuthorization: Bearer <accessToken>
到達服務端時,Spring Security 會按如下步驟進行處理:
- 進入
SecurityFilterChain- 請求首先會進入配置好的安全過濾器鏈;
- 因為啟用了
.oauth2ResourceServer().jwt(),鏈中包含 Bearer Token 相關過濾器。 - 從請求頭中提取 Bearer Token
- 過濾器內(nèi)部使用
BearerTokenResolver(默認實現(xiàn)為DefaultBearerTokenResolver): - 從請求頭里讀取
Authorization;
- 匹配
Bearer前綴;- 截取出
<accessToken>部分。 - 創(chuàng)建未認證的
Authentication并委托給AuthenticationManager - 根據(jù) Token 創(chuàng)建一個
BearerTokenAuthenticationToken,此時它還處于“未認證”狀態(tài); - 將該對象交由
AuthenticationManager(內(nèi)部委托給JwtAuthenticationProvider)處理。 JwtAuthenticationProvider使用JwtDecoder進行校驗與解析JwtAuthenticationProvider注入的就是我們前面定義的JwtDecoderBean;
- 截取出
- 調(diào)用
jwtDecoder.decode(accessToken):- 使用 RSA 公鑰校驗簽名;
- 校驗 Token 是否過期、是否生效(
exp/nbf等); - 解析出完整的 Claim 集合并構造
Jwt對象。 - 構造認證后的
JwtAuthenticationToken,寫入SecurityContext - 若校驗通過,
JwtAuthenticationProvider會創(chuàng)建一個認證成功的JwtAuthenticationToken:
principal:解析得到的Jwt;authorities:可根據(jù) Claim 映射出的權限列表(本項目中暫未做復雜映射)。- 將這個
Authentication寫入當前線程的SecurityContextHolder中,代表“當前請求已認證”。
- 校驗失敗時的處理
- 若沒有 Token、Token 非法或過期,
JwtDecoder會拋出異常; - 資源服務器模塊會返回 401 或 403 響應,Controller 不會被執(zhí)行。
- 若沒有 Token、Token 非法或過期,
六、Controller 層:@AuthenticationPrincipal Jwt的注入機制
回到 AuthController 中的 /me 接口:
@GetMapping("/me")
public AuthUserResponse me(@AuthenticationPrincipal Jwt jwt) {
long userId = jwtService.extractUserId(jwt);
return authService.me(userId);
}
這里的 jwt 參數(shù)是如何被自動注入的?
- Spring MVC 參數(shù)解析 + Spring Security 協(xié)作
- Spring MVC 調(diào)用 Handler 方法前,會為每個參數(shù)尋找“數(shù)據(jù)來源”;
- 當發(fā)現(xiàn)參數(shù)上有
@AuthenticationPrincipal注解時,會啟用專門的參數(shù)解析器: - 從
SecurityContextHolder.getContext().getAuthentication()中獲取當前Authentication; - 默認取
authentication.getPrincipal();
- 嘗試匹配/轉換為方法參數(shù)所聲明的類型。
- 在資源服務器 JWT 場景下,principal 正是
Jwt - 前面的認證流程中,
JwtAuthenticationProvider構造的是JwtAuthenticationToken; - 其
getPrincipal()返回的就是org.springframework.security.oauth2.jwt.Jwt對象; - 因此,當方法參數(shù)聲明為
@AuthenticationPrincipal Jwt jwt時,類型就剛好匹配,可以直接注入。
- 在資源服務器 JWT 場景下,principal 正是
- 業(yè)務層通過 Claim 完成“當前用戶”的識別
- 本項目在簽發(fā) Token 時把用戶 ID 放在
uidClaim 中; - 因此在
/me中,可以通過jwtService.extractUserId(jwt)從 Claim 集合中讀取uid,作為當前登錄用戶的唯一標識; - 后續(xù)調(diào)用
authService.me(userId)查詢用戶信息并返回。
- 本項目在簽發(fā) Token 時把用戶 ID 放在
七、口頭表達總結:如何在面試中講清這條鏈路
如果在面試中被問到“你這個項目里 JWT 是如何鑒權的?@AuthenticationPrincipal Jwt 的 Jwt 從哪來的?”,可以這樣組織回答:
我這邊是基于 Spring Security 的 OAuth2 Resource Server,做了基于 JWT 的無狀態(tài)鑒權。不用 Session 記錄登錄狀態(tài),靠請求頭中攜帶的 token 進行鑒權、記錄登錄狀態(tài)。配置層面,配置基于 RSA 公鑰的 JWT 的解碼器注入 ioc 容器中。在 SpringSecurity 配置類中啟用資源服務器的 JWT 模式,安全過濾器鏈中會添加一個 Bearer Token 過濾器,所有非白名單接口都會先走一遍這個過濾器,從請求頭里提取 Token ,交由解碼器進行校驗與解析,若沒有 Token、Token 非法或過期,解碼器會拋出異常,返回 401 或 403 響應,Controller 不會被執(zhí)行。校驗通過后會構造一個 Jwt 對象交由線程上下文進行管理。Controller 里使用
@AuthenticationPrincipal Jwt jwt這種方式,Spring MVC 會從SecurityContext中拿出這個 Jwt 對象注入到參數(shù)里。到這一步鑒權完成,然后通過我封裝的方法,可以從 Jwt 對象的聲明中(Claim)里取出登錄的用戶 ID,整個過程是完全無狀態(tài)的。
八、總結
整條從 Authorization: Bearer 到 @AuthenticationPrincipal Jwt 的鏈路,可以概括為四層:
- 配置層:
SecurityFilterChain啟用 JWT Resource Server 模式,AuthConfiguration提供JwtEncoder/JwtDecoderBean。 - 過濾器層:Bearer Token 過濾器從請求頭里提取 Token,交由
JwtAuthenticationProvider使用JwtDecoder校驗與解析。 - 安全上下文層:認證通過后,
JwtAuthenticationToken被寫入SecurityContext,代表當前線程的認證狀態(tài)。 - 控制器層:
@AuthenticationPrincipal Jwt從SecurityContext中獲取 principal 注入到方法參數(shù),業(yè)務通過 Claim 完成用戶識別。
掌握這條鏈路之后,我們不僅能在項目中更好地調(diào)試和擴展安全邏輯,也能在面試中把“Spring Security + JWT 鑒權”講得更清晰、更體系化。
到此這篇關于一次完整的Spring Security JWT 鑒權鏈路解析的文章就介紹到這了,更多相關Spring Security JWT 鑒權鏈路內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權認證
- SpringBoot集成Spring Security用JWT令牌實現(xiàn)登錄和鑒權的方法
- SpringBoot集成SpringSecurity和JWT做登陸鑒權的實現(xiàn)
- Spring?Boot基于?JWT?優(yōu)化?Spring?Security?無狀態(tài)登錄實戰(zhàn)指南
- SpringSecurity和jwt實現(xiàn)登錄及權限認證功能
- SpringSecurity+JWT實現(xiàn)登錄流程分析
- springSecurity+jwt使用小結
相關文章
java中兩個字符串的拼接、整數(shù)相加和浮點數(shù)相加實現(xiàn)代碼
這篇文章主要為大家介紹java中從鍵盤讀取用戶輸入兩個字符串,實現(xiàn)這兩個字符串的拼接、整數(shù)相加和浮點數(shù)相加,并輸出結果,需要的朋友可以參考下2021-05-05
詳解Spring Security的Web應用和指紋登錄實踐
這篇文章主要介紹了詳解Spring Security的Web應用和指紋登錄實踐,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03
詳解如何在SpringBoot中優(yōu)雅地重試調(diào)用第三方API
在實際的應用中,我們經(jīng)常需要調(diào)用第三方API來獲取數(shù)據(jù)或執(zhí)行某些操作,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2023-12-12

