Spring Security JWT 鑒權(quán)鏈路完整解析
一次完整的 Spring Security JWT 鑒權(quán)鏈路解析
在實(shí)際項(xiàng)目中,我們經(jīng)常會(huì)在 Controller 里寫出這樣一個(gè)方法簽名:
@GetMapping("/me")
public AuthUserResponse me(@AuthenticationPrincipal Jwt jwt) {
long userId = jwtService.extractUserId(jwt);
return authService.me(userId);
}
看起來(lái) Spring 能“憑空”把一個(gè) Jwt 對(duì)象塞進(jìn)方法參數(shù)里,還自動(dòng)從請(qǐng)求頭里的 Authorization: Bearer 提取 Token 并完成校驗(yàn)。
這篇文章就結(jié)合項(xiàng)目,從配置到代碼,串起這條鑒權(quán)鏈路的每一步。
一、整體流程總覽
從前端調(diào)用 /api/v1/auth/me 開始,到方法參數(shù)里拿到 Jwt jwt,整個(gè)鏈路可以概括為:
- 前端發(fā)請(qǐng)求:HTTP 請(qǐng)求頭里帶
Authorization: Bearer <accessToken>。 - Security 過(guò)濾器攔截:
SecurityFilterChain中的 Bearer Token 過(guò)濾器從請(qǐng)求頭里截出<accessToken>。 - 交給
JwtDecoder校驗(yàn)解析:過(guò)濾器調(diào)用配置好的JwtDecoder(基于 RSA 公鑰的NimbusJwtDecoder),做簽名、過(guò)期等校驗(yàn),解析出 Claim,得到一個(gè)Jwt對(duì)象。 - 封裝
Authentication放入SecurityContext:框架把Jwt封裝成JwtAuthenticationToken,寫入當(dāng)前線程的SecurityContext。 - Controller 使用
@AuthenticationPrincipal Jwt注入當(dāng)前用戶 Token:@AuthenticationPrincipal從SecurityContext里取出 principal(類型為Jwt)注入到方法參數(shù)。 - 業(yè)務(wù)層解析用戶 ID:
jwtService.extractUserId(jwt)從 Claim 中讀取uid,實(shí)現(xiàn)當(dāng)前登錄用戶識(shí)別與后續(xù)業(yè)務(wù)處理。
下面按照“配置層 → 過(guò)濾器層 → 控制器層”的順序,一步步展開。
二、開啟資源服務(wù)器模式:SecurityFilterChain的核心配置
項(xiàng)目中 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)容:首頁(yè) Feed 不需要登錄
.requestMatchers("/api/v1/knowposts/feed").permitAll()
// 知文詳情等其他白名單接口...
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}這里有幾個(gè)關(guān)鍵點(diǎn):
- 無(wú)狀態(tài)會(huì)話:
SessionCreationPolicy.STATELESS- 服務(wù)端不使用 Session 記住“誰(shuí)登錄過(guò)”,每個(gè)請(qǐng)求都必須自帶 Token。
- 訪問(wèn)控制:
- 白名單接口使用
permitAll()直接放行; - 其他接口通過(guò)
.anyRequest().authenticated()強(qiáng)制要求認(rèn)證。
- 白名單接口使用
- 啟用 JWT 資源服務(wù)器模式:
oauth2ResourceServer(oauth -> oauth.jwt())- 告訴 Spring Security 本應(yīng)用是一個(gè) OAuth2 Resource Server;
- 請(qǐng)求需要通過(guò) Bearer Token + JWT 的方式來(lái)進(jìn)行認(rèn)證;
- 框架自動(dòng)往過(guò)濾器鏈里加上 Bearer Token 相關(guān)的過(guò)濾器和認(rèn)證器。
這樣一來(lái),我們就不需要自己在每個(gè)接口里手動(dòng)解析請(qǐng)求頭,整個(gè) Token 解析與校驗(yàn)流程都由 Spring Security 統(tǒng)一接管。
三、JWT 編解碼 Bean:JwtEncoder與JwtDecoder
要讓資源服務(wù)器真正“識(shí)別”JWT,我們必須告訴它如何簽發(fā)和校驗(yàn) 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;
- 構(gòu)造基于 Nimbus 的
JwtEncoder; - 用于登錄/注冊(cè)成功后簽發(fā) Access Token 與 Refresh Token。
JwtDecoder- 只讀取 RSA 公鑰;
- 創(chuàng)建
NimbusJwtDecoder,用于校驗(yàn)簽名、過(guò)期時(shí)間等信息并解析 Claim。
關(guān)鍵點(diǎn):
一旦容器中存在一個(gè) JwtDecoder Bean,oauth2ResourceServer().jwt() 會(huì)自動(dòng)使用它來(lái)完成 JWT 的解析與驗(yàn)證,無(wú)需手動(dòng)再把 JwtDecoder 綁定到過(guò)濾器上。
四、自定義 JWT 服務(wù):簽發(fā)與解析的業(yè)務(wù)封裝JwtService
在業(yè)務(wù)層,我們通過(guò) JwtService 把 Token 的簽發(fā)與解析從 Controller 和 Service 中抽離出來(lái):
@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構(gòu)造帶有uid、token_type、jti等 Claim 的 Token; - Access Token 與 Refresh Token 使用不同的 TTL 與類型標(biāo)記。
- 使用
- 解析階段:
- 可以通過(guò)
decode()手動(dòng)解析某個(gè) Token(例如刷新或登出時(shí)用); - 對(duì)于
/me等需要“當(dāng)前用戶”的接口,更多使用extractUserId(jwt),從已經(jīng)被框架解析過(guò)的Jwt中讀取uidClaim。
- 可以通過(guò)
五、請(qǐng)求進(jìn)入時(shí):Filter 如何一步步完成 JWT 鑒權(quán)
當(dāng)一個(gè)請(qǐng)求攜帶:
GET /api/v1/auth/meAuthorization: Bearer <accessToken>
到達(dá)服務(wù)端時(shí),Spring Security 會(huì)按如下步驟進(jìn)行處理:
- 進(jìn)入
SecurityFilterChain- 請(qǐng)求首先會(huì)進(jìn)入配置好的安全過(guò)濾器鏈;
- 因?yàn)閱⒂昧?
.oauth2ResourceServer().jwt(),鏈中包含 Bearer Token 相關(guān)過(guò)濾器。 - 從請(qǐng)求頭中提取 Bearer Token
- 過(guò)濾器內(nèi)部使用
BearerTokenResolver(默認(rèn)實(shí)現(xiàn)為DefaultBearerTokenResolver): - 從請(qǐng)求頭里讀取
Authorization;
- 匹配
Bearer前綴;- 截取出
<accessToken>部分。 - 創(chuàng)建未認(rèn)證的
Authentication并委托給AuthenticationManager - 根據(jù) Token 創(chuàng)建一個(gè)
BearerTokenAuthenticationToken,此時(shí)它還處于“未認(rèn)證”狀態(tài); - 將該對(duì)象交由
AuthenticationManager(內(nèi)部委托給JwtAuthenticationProvider)處理。 JwtAuthenticationProvider使用JwtDecoder進(jìn)行校驗(yàn)與解析JwtAuthenticationProvider注入的就是我們前面定義的JwtDecoderBean;
- 截取出
- 調(diào)用
jwtDecoder.decode(accessToken):- 使用 RSA 公鑰校驗(yàn)簽名;
- 校驗(yàn) Token 是否過(guò)期、是否生效(
exp/nbf等); - 解析出完整的 Claim 集合并構(gòu)造
Jwt對(duì)象。 - 構(gòu)造認(rèn)證后的
JwtAuthenticationToken,寫入SecurityContext - 若校驗(yàn)通過(guò),
JwtAuthenticationProvider會(huì)創(chuàng)建一個(gè)認(rèn)證成功的JwtAuthenticationToken:
principal:解析得到的Jwt;authorities:可根據(jù) Claim 映射出的權(quán)限列表(本項(xiàng)目中暫未做復(fù)雜映射)。- 將這個(gè)
Authentication寫入當(dāng)前線程的SecurityContextHolder中,代表“當(dāng)前請(qǐng)求已認(rèn)證”。
- 校驗(yàn)失敗時(shí)的處理
- 若沒(méi)有 Token、Token 非法或過(guò)期,
JwtDecoder會(huì)拋出異常; - 資源服務(wù)器模塊會(huì)返回 401 或 403 響應(yīng),Controller 不會(huì)被執(zhí)行。
- 若沒(méi)有 Token、Token 非法或過(guò)期,
六、Controller 層:@AuthenticationPrincipal Jwt的注入機(jī)制
回到 AuthController 中的 /me 接口:
@GetMapping("/me")
public AuthUserResponse me(@AuthenticationPrincipal Jwt jwt) {
long userId = jwtService.extractUserId(jwt);
return authService.me(userId);
}
這里的 jwt 參數(shù)是如何被自動(dòng)注入的?
- Spring MVC 參數(shù)解析 + Spring Security 協(xié)作
- Spring MVC 調(diào)用 Handler 方法前,會(huì)為每個(gè)參數(shù)尋找“數(shù)據(jù)來(lái)源”;
- 當(dāng)發(fā)現(xiàn)參數(shù)上有
@AuthenticationPrincipal注解時(shí),會(huì)啟用專門的參數(shù)解析器: - 從
SecurityContextHolder.getContext().getAuthentication()中獲取當(dāng)前Authentication; - 默認(rèn)取
authentication.getPrincipal();
- 嘗試匹配/轉(zhuǎn)換為方法參數(shù)所聲明的類型。
- 在資源服務(wù)器 JWT 場(chǎng)景下,principal 正是
Jwt - 前面的認(rèn)證流程中,
JwtAuthenticationProvider構(gòu)造的是JwtAuthenticationToken; - 其
getPrincipal()返回的就是org.springframework.security.oauth2.jwt.Jwt對(duì)象; - 因此,當(dāng)方法參數(shù)聲明為
@AuthenticationPrincipal Jwt jwt時(shí),類型就剛好匹配,可以直接注入。
- 在資源服務(wù)器 JWT 場(chǎng)景下,principal 正是
- 業(yè)務(wù)層通過(guò) Claim 完成“當(dāng)前用戶”的識(shí)別
- 本項(xiàng)目在簽發(fā) Token 時(shí)把用戶 ID 放在
uidClaim 中; - 因此在
/me中,可以通過(guò)jwtService.extractUserId(jwt)從 Claim 集合中讀取uid,作為當(dāng)前登錄用戶的唯一標(biāo)識(shí); - 后續(xù)調(diào)用
authService.me(userId)查詢用戶信息并返回。
- 本項(xiàng)目在簽發(fā) Token 時(shí)把用戶 ID 放在
七、口頭表達(dá)總結(jié):如何在面試中講清這條鏈路
如果在面試中被問(wèn)到“你這個(gè)項(xiàng)目里 JWT 是如何鑒權(quán)的?@AuthenticationPrincipal Jwt 的 Jwt 從哪來(lái)的?”,可以這樣組織回答:
我這邊是基于 Spring Security 的 OAuth2 Resource Server,做了基于 JWT 的無(wú)狀態(tài)鑒權(quán)。不用 Session 記錄登錄狀態(tài),靠請(qǐng)求頭中攜帶的 token 進(jìn)行鑒權(quán)、記錄登錄狀態(tài)。配置層面,配置基于 RSA 公鑰的 JWT 的解碼器注入 ioc 容器中。在 SpringSecurity 配置類中啟用資源服務(wù)器的 JWT 模式,安全過(guò)濾器鏈中會(huì)添加一個(gè) Bearer Token 過(guò)濾器,所有非白名單接口都會(huì)先走一遍這個(gè)過(guò)濾器,從請(qǐng)求頭里提取 Token ,交由解碼器進(jìn)行校驗(yàn)與解析,若沒(méi)有 Token、Token 非法或過(guò)期,解碼器會(huì)拋出異常,返回 401 或 403 響應(yīng),Controller 不會(huì)被執(zhí)行。校驗(yàn)通過(guò)后會(huì)構(gòu)造一個(gè) Jwt 對(duì)象交由線程上下文進(jìn)行管理。Controller 里使用
@AuthenticationPrincipal Jwt jwt這種方式,Spring MVC 會(huì)從SecurityContext中拿出這個(gè) Jwt 對(duì)象注入到參數(shù)里。到這一步鑒權(quán)完成,然后通過(guò)我封裝的方法,可以從 Jwt 對(duì)象的聲明中(Claim)里取出登錄的用戶 ID,整個(gè)過(guò)程是完全無(wú)狀態(tài)的。
八、總結(jié)
整條從 Authorization: Bearer 到 @AuthenticationPrincipal Jwt 的鏈路,可以概括為四層:
- 配置層:
SecurityFilterChain啟用 JWT Resource Server 模式,AuthConfiguration提供JwtEncoder/JwtDecoderBean。 - 過(guò)濾器層:Bearer Token 過(guò)濾器從請(qǐng)求頭里提取 Token,交由
JwtAuthenticationProvider使用JwtDecoder校驗(yàn)與解析。 - 安全上下文層:認(rèn)證通過(guò)后,
JwtAuthenticationToken被寫入SecurityContext,代表當(dāng)前線程的認(rèn)證狀態(tài)。 - 控制器層:
@AuthenticationPrincipal Jwt從SecurityContext中獲取 principal 注入到方法參數(shù),業(yè)務(wù)通過(guò) Claim 完成用戶識(shí)別。
掌握這條鏈路之后,我們不僅能在項(xiàng)目中更好地調(diào)試和擴(kuò)展安全邏輯,也能在面試中把“Spring Security + JWT 鑒權(quán)”講得更清晰、更體系化。
到此這篇關(guān)于一次完整的Spring Security JWT 鑒權(quán)鏈路解析的文章就介紹到這了,更多相關(guān)Spring Security JWT 鑒權(quán)鏈路內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- Spring?Boot基于?JWT?優(yōu)化?Spring?Security?無(wú)狀態(tài)登錄實(shí)戰(zhàn)指南
- SpringSecurity和jwt實(shí)現(xiàn)登錄及權(quán)限認(rèn)證功能
- SpringSecurity+JWT實(shí)現(xiàn)登錄流程分析
- springSecurity+jwt使用小結(jié)
相關(guān)文章
java異常:異常處理--try-catch結(jié)構(gòu)詳解
今天小編就為大家分享一篇關(guān)于Java異常處理之try...catch...finally詳解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2021-09-09
SpringEvents與異步事件驅(qū)動(dòng)案例詳解
本文深入探討了SpringBoot中的事件驅(qū)動(dòng)架構(gòu),特別是通過(guò)Spring事件機(jī)制實(shí)現(xiàn)組件解耦和系統(tǒng)擴(kuò)展性增強(qiáng),介紹了事件的發(fā)布者、事件本身、事件監(jiān)聽器和事件處理器的概念,感興趣的朋友跟隨小編一起看看吧2024-09-09
ShardingSphere 分庫(kù)分表原理與Spring Boot集成實(shí)踐方案
本文探討了ShardingSphere分庫(kù)分表原理及其Spring Boot集成方案,詳細(xì)闡述了SQL解析、分片路由、SQL改寫、結(jié)果歸并和事務(wù)管理等關(guān)鍵技術(shù)原理,感興趣的朋友跟隨小編一起看看吧2026-02-02
Java查找不重復(fù)無(wú)序數(shù)組中是否存在兩個(gè)數(shù)字的和為某個(gè)值
今天小編就為大家分享一篇關(guān)于Java查找不重復(fù)無(wú)序數(shù)組中是否存在兩個(gè)數(shù)字的和為某個(gè)值,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01
淺談Spring中幾個(gè)PostProcessor的區(qū)別與聯(lián)系
這篇文章主要介紹了淺談Spring中幾個(gè)PostProcessor的區(qū)別與聯(lián)系,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

