Spring Security 深度學習(六): RESTful API 安全與 JWT

在這里插入圖片描述

目錄

    • 1. 引言:無狀態認證的崛起
    • 2. JWT (JSON Web Token) 核心概念
      • 2.1 什么是JWT?
      • 2.2 JWT的組成:Header, Payload, Signature
      • 2.3 JWT的工作原理
      • 2.4 JWT的優缺點與適用場景
    • 3. Spring Security中的JWT集成策略
      • 3.1 禁用Session管理與CSRF防護
      • 3.2 JWT認證流程概述
    • 4. 實戰演練:構建JWT認證系統
      • 4.1 引入JWT庫依賴
      • 4.2 JWT工具類:生成與解析Token
      • 4.3 自定義 JwtAuthenticationToken
      • 4.4 自定義 JwtAuthenticationConverter (或 AuthenticationProvider)
      • 4.5 自定義 JwtAuthenticationFilter
      • 4.6 更新 SecurityFilterChain 配置,集成JWT過濾器
      • 4.7 改造登錄接口,返回JWT
      • 4.8 認證失敗與權限不足的自定義處理
      • 4.9 測試JWT認證流程
    • 5. JWT的安全性與挑戰
      • 5.1 Token過期與刷新機制
      • 5.2 JWT注銷/黑名單機制
      • 5.3 密鑰管理
      • 5.4 防止令牌盜用
    • 6. 常見陷阱與注意事項
    • 7. 階段總結

1. 引言:無狀態認證的崛起

傳統的Web應用通常依賴于服務器端的HTTP Session來維護用戶狀態。每次用戶登錄后,服務器會創建一個Session并將其Session ID通過Cookie發送給客戶端。客戶端在后續請求中攜帶這個Cookie,服務器通過Session ID查找對應的Session,從而識別用戶身份。

然而,這種基于Session的方式在以下場景中面臨挑戰:

  • 前后端分離: 前端(React, Vue, Angular)和后端(Spring Boot API)是獨立的,它們之間可能存在跨域請求。Cookie通常受同源策略限制,且在前端應用中直接操作Cookie不方便。
  • 微服務架構: 用戶請求可能需要經過多個微服務,Session的共享和管理(例如使用Sticky Session或Redis共享Session)變得復雜且增加了系統耦合度。
  • 移動應用/第三方應用: 移動客戶端不能很好地支持Cookie,更傾向于通過Authorization Header傳遞憑證。
  • 水平擴展: 當服務器集群需要水平擴展時,Session共享成為瓶頸。

無狀態認證應運而生。它意味著服務器不再存儲用戶會話信息,每次請求都攜帶完整的認證憑證。JWT (JSON Web Token) 是實現無狀態認證的主流方案之一。

2. JWT (JSON Web Token) 核心概念

2.1 什么是JWT?

JWT是一種開放標準(RFC 7519),它定義了一種緊湊且自包含的方式,用于在各方之間安全地傳輸信息。這些信息以JSON對象的形式傳輸,可以被數字簽名,從而可以驗證其真實性和完整性。

  • 緊湊: JWT的體積很小,可以通過URL、POST參數或HTTP頭輕松傳輸。
  • 自包含: JWT包含了所有必要的用戶信息(通常是用戶ID、角色、權限等),服務器無需查詢數據庫即可獲取這些信息。
  • 安全: JWT可以通過數字簽名進行驗證,確保其未被篡改。

2.2 JWT的組成:Header, Payload, Signature

一個JWT通常由三部分組成,用.分隔:Header.Payload.Signature

A. Header (頭部)
通常包含兩個信息:

  • alg (algorithm):簽名算法,如HMAC SHA256 (HS256) 或 RSA (RS256)。
  • typ (type):Token類型,通常是JWT
{"alg": "HS256","typ": "JWT"
}

Header會被Base64Url編碼。

B. Payload (載荷)
包含聲明 (claims),是關于實體(通常是用戶)和附加數據的斷言。聲明分為三類:

  • Registered claims (注冊聲明): 預定義的一些聲明,非強制,但推薦使用,例如:
    • iss (issuer):頒發者
    • exp (expiration time):過期時間
    • sub (subject):主題(通常是用戶ID)
    • aud (audience):受眾
    • iat (issued at):簽發時間
  • Public claims (公共聲明): 可以在JWT中自由定義的聲明,但為了避免沖突,應該在IANA JWT Registry中注冊,或者將其定義為URI。
  • Private claims (私有聲明): 約定俗成的聲明,用于在特定方之間共享信息,既不是注冊聲明也不是公共聲明。例如,可以包含用戶角色、權限列表等業務信息。
{"sub": "1234567890","name": "John Doe","iat": 1516239022,"exp": 1516242622, // 簽發時間 + 有效期"roles": ["USER", "ADMIN"] // 私有聲明
}

Payload也會被Base64Url編碼。

C. Signature (簽名)
用于驗證Token的發送者,并確保Token在傳輸過程中沒有被篡改。
簽名是使用Header中指定的算法(例如HS256),將Base64Url編碼后的Header、Base64Url編碼后的Payload和密鑰(secret)進行加密計算得到。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

2.3 JWT的工作原理

  1. 用戶登錄: 用戶使用用戶名和密碼向認證服務器(應用后端)發送登錄請求。
  2. 生成JWT: 認證服務器驗證用戶憑證。如果驗證成功,根據用戶ID、角色、權限等信息生成一個JWT,并用一個密鑰進行簽名。
  3. 返回JWT: 服務器將生成的JWT返回給客戶端(通常在HTTP響應體中)。
  4. 客戶端存儲JWT: 客戶端接收到JWT后,通常將其存儲在本地存儲(如LocalStorage或SessionStorage)中。
  5. 訪問受保護資源: 客戶端在后續每次訪問受保護的API時,都會在HTTP請求頭的Authorization字段中攜帶JWT,格式為Authorization: Bearer <JWT>
  6. 驗證JWT: 資源服務器(應用后端)接收到請求后,從Authorization頭中提取JWT。然后,它使用之前用于簽名的密鑰驗證JWT的簽名、檢查Token是否過期,以及解析其中的聲明(如用戶ID、權限)。
  7. 授權與響應: 如果JWT有效且用戶具有所需權限,服務器處理請求并返回數據。如果JWT無效或過期,或者用戶權限不足,則返回錯誤(如401 Unauthorized或403 Forbidden)。

2.4 JWT的優缺點與適用場景

優點:

  • 無狀態: 服務器無需存儲Session,易于水平擴展,適用于微服務。
  • 緊湊自包含: 包含了所有必要的用戶信息,減少了數據庫查詢。
  • 跨域友好: 不依賴Cookie,易于跨域請求。
  • 移動兼容性: 廣泛應用于移動應用。

缺點:

  • Token無法實時注銷: JWT一旦簽發,在其有效期內都是有效的,服務器端無法強制使其失效(除非引入黑名單機制)。
  • Token過大: 如果Payload中包含太多信息,Token會變大,增加請求頭大小。
  • 安全性考量:
    • 密鑰安全: 簽名密鑰一旦泄露,攻擊者可以偽造Token。
    • 傳輸安全: JWT應始終通過HTTPS傳輸,防止Token被截獲。
    • XSS風險: 如果存儲在LocalStorage,容易受到XSS攻擊。
    • 無CSRF防護: 因為不依賴Session Cookie,JWT本身不提供CSRF防護,因此無需特別開啟CSRF。

適用場景:

  • 前后端分離的Web應用。
  • 微服務架構中的API認證。
  • 移動應用和桌面應用。
  • 第三方OAuth2/OpenID Connect認證。

3. Spring Security中的JWT集成策略

在Spring Security中集成JWT,通常需要進行以下調整:

3.1 禁用Session管理與CSRF防護

由于JWT是無狀態的,我們不再需要Spring Security的Session管理和CSRF防護功能。

            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 設置為無狀態).csrf(csrf -> csrf.disable()) // 禁用CSRF防護

3.2 JWT認證流程概述

  1. JWT生成: 在用戶登錄成功后,后端生成JWT并返回。
  2. JWT傳輸: 客戶端將JWT存儲起來,并在每次請求時通過Authorization: Bearer <JWT>請求頭發送。
  3. JWT解析與驗證: Spring Security過濾器鏈中會插入一個自定義的JWT過濾器:
    • 它攔截所有請求,從Authorization頭中提取JWT。
    • 使用預設的密鑰解析并驗證JWT的簽名和有效期。
    • 如果驗證成功,從JWT中提取用戶ID和權限,創建Authentication對象。
    • Authentication對象設置到SecurityContextHolder中。
  4. 授權: 后續的Spring Security授權過濾器(如FilterSecurityInterceptor)會根據SecurityContextHolder中的認證信息進行授權決策。

4. 實戰演練:構建JWT認證系統

我們將改造之前的項目,實現JWT認證。

4.1 引入JWT庫依賴

我們將使用jjwt庫來處理JWT。

        <!-- JJWT (JWT Library) --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.12.5</version> </dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.12.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.12.5</version><scope>runtime</scope></dependency>

4.2 JWT工具類:生成與解析Token

創建一個工具類來處理JWT的生成、解析和驗證。

package com.example.springsecuritystage1.util;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.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;@Component
public class JwtUtil {// 密鑰。生產環境務必從安全通道獲取,不能硬編碼。@Value("${jwt.secret:thisismyjwtsecretkeythatiwilluseforsigningandvalidatingtokensanditshouldbeverylongandcomplex}")private String secret;// JWT有效期 (毫秒),這里設置為1小時@Value("${jwt.expiration:3600000}")private long expiration; // 1 hourprivate SecretKey getSigningKey() {// 使用 HS256 算法生成密鑰return Keys.hmacShaKeyFor(secret.getBytes());}// 生成Tokenpublic String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();// 將用戶權限添加到claims中List<String> authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());claims.put("authorities", authorities);return createToken(claims, userDetails.getUsername());}private String createToken(Map<String, Object> claims, String subject) {Date now = new Date();Date expiryDate = new Date(now.getTime() + expiration);return Jwts.builder().setClaims(claims) // 自定義聲明.setSubject(subject) // 用戶名.setIssuedAt(now) // 簽發時間.setExpiration(expiryDate) // 過期時間.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 簽名算法和密鑰.compact();}// 從Token中獲取所有聲明public Claims extractAllClaims(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}// 從Token中獲取用戶名public String extractUsername(String token) {return extractAllClaims(token).getSubject();}// 從Token中獲取過期時間public Date extractExpiration(String token) {return extractAllClaims(token).getExpiration();}// 檢查Token是否過期private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}// 驗證Token是否有效public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}// 額外的:從Token中獲取權限@SuppressWarnings("unchecked")public List<String> extractAuthorities(String token) {return (List<String>) extractAllClaims(token).get("authorities");}
}

application.yml中添加JWT配置:

jwt:secret: your_jwt_secret_key_that_is_very_long_and_complex_and_should_be_kept_secure_in_production # 至少32位,生產環境務必使用更長更隨機的密鑰expiration: 3600000 # 1小時,單位毫秒

4.3 自定義 JwtAuthenticationToken

ApiKeyAuthenticationToken類似,我們需要一個Authentication實現來承載從JWT解析出的認證信息。

package com.example.springsecuritystage1.security.token;import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;import java.util.Collection;public class JwtAuthenticationToken extends AbstractAuthenticationToken {private final Object principal; // 用戶名或UserDetails對象private String credentials; // JWT字符串本身public JwtAuthenticationToken(String jwtToken) {super(null);this.principal = null; // 初始時principal是nullthis.credentials = jwtToken; // JWT Token作為憑證setAuthenticated(false);}public JwtAuthenticationToken(Object principal, String jwtToken, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = jwtToken;setAuthenticated(true);}@Overridepublic Object getCredentials() {return credentials;}@Overridepublic Object getPrincipal() {return principal;}
}

4.4 自定義 JwtAuthenticationConverter (或 AuthenticationProvider)

Spring Security 6.x 推薦使用BearerTokenAuthenticationConverterReactiveJwtDecoder等用于OAuth2 Resource Server,但對于自定義的JWT,我們可以繼續使用AuthenticationProvider

package com.example.springsecuritystage1.security.provider;import com.example.springsecuritystage1.security.token.JwtAuthenticationToken;
import com.example.springsecuritystage1.service.CustomUserDetailsService; // 你的UserDetailsService
import com.example.springsecuritystage1.util.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {private final JwtUtil jwtUtil;private final CustomUserDetailsService userDetailsService; // 用于加載用戶詳情public JwtAuthenticationProvider(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) {this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;String jwt = (String) jwtAuthenticationToken.getCredentials();try {String username = jwtUtil.extractUsername(jwt);List<String> authoritiesStrings = jwtUtil.extractAuthorities(jwt); // 從JWT中提取權限// 可以選擇從數據庫再次加載UserDetails,以確保用戶狀態最新// 或者僅僅使用JWT中的信息構建User對象UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails)) {Set<SimpleGrantedAuthority> authorities = authoritiesStrings.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());return new JwtAuthenticationToken(userDetails, jwt, authorities);} else {throw new BadCredentialsException("Invalid JWT token");}} catch (ExpiredJwtException e) {throw new BadCredentialsException("JWT Token has expired", e);} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {throw new BadCredentialsException("Invalid JWT Token", e);}}@Overridepublic boolean supports(Class<?> authentication) {return JwtAuthenticationToken.class.isAssignableFrom(authentication);}
}

注意:JwtAuthenticationProvider中,我們從JWT中提取了權限信息。但為了確保用戶狀態(如enabledaccountNonLocked)是最新的,我們仍然通過userDetailsService.loadUserByUsername(username)從數據庫加載了完整的UserDetails。如果JWT中包含足夠的信息且不關心實時狀態,可以直接基于JWT信息構建User對象。

4.5 自定義 JwtAuthenticationFilter

這個過濾器負責攔截請求,提取JWT,并將其提交給AuthenticationManager

package com.example.springsecuritystage1.filter;import com.example.springsecuritystage1.security.token.JwtAuthenticationToken;
import com.example.springsecuritystage1.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;// JWT 認證過濾器
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final AuthenticationManager authenticationManager; // 注入 AuthenticationManagerpublic JwtAuthenticationFilter(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// 1. 從 Authorization header 中獲取 JWT TokenString authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);String jwt = null;if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {jwt = authorizationHeader.substring(7); // 提取Bearer Token}// 如果沒有JWT,或者SecurityContext中已經有認證信息(例如通過Session登錄),則跳過if (jwt == null || SecurityContextHolder.getContext().getAuthentication() != null) {filterChain.doFilter(request, response);return;}try {// 2. 創建一個未認證的 JwtAuthenticationTokenJwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(jwt);// 3. 將Token提交給 AuthenticationManager 進行認證Authentication authentication = authenticationManager.authenticate(authenticationToken);// 4. 認證成功,將認證信息存入 SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(authentication);System.out.println("JWT authenticated successfully for: " + authentication.getName());} catch (Exception e) {// 認證失敗,清除SecurityContext,并返回401 UnauthorizedSecurityContextHolder.clearContext();response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("JWT authentication failed: " + e.getMessage());return; // 阻止請求繼續往下走}// 繼續過濾器鏈filterChain.doFilter(request, response);}
}

4.6 更新 SecurityFilterChain 配置,集成JWT過濾器

現在,我們需要在CustomSecurityConfig中添加JwtAuthenticationProviderAuthenticationManager,并將JwtAuthenticationFilter插入到過濾器鏈中。同時,禁用Session管理和CSRF防護。

package com.example.springsecuritystage1.config;// ... 省略其他 imports
import com.example.springsecuritystage1.filter.ApiKeyAuthenticationFilter;
import com.example.springsecuritystage1.filter.JwtAuthenticationFilter; // 導入 JWT 過濾器
import com.example.springsecuritystage1.security.provider.ApiKeyAuthenticationProvider;
import com.example.springsecuritystage1.security.provider.JwtAuthenticationProvider; // 導入 JWT Provider
import com.example.springsecuritystage1.util.JwtUtil; // 導入 JWT 工具類
import org.springframework.http.HttpMethod; // 導入
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.http.SessionCreationPolicy; // 導入@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class CustomSecurityConfig {private final DataSource dataSource;private final UserDetailsService userDetailsService;private final PasswordEncoder passwordEncoder;private final ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;private final JwtAuthenticationProvider jwtAuthenticationProvider; // 注入 JWT Providerprivate final JwtUtil jwtUtil; // 注入 JWTUtilpublic CustomSecurityConfig(DataSource dataSource,UserDetailsService userDetailsService,PasswordEncoder passwordEncoder,ApiKeyAuthenticationProvider apiKeyAuthenticationProvider,JwtAuthenticationProvider jwtAuthenticationProvider,JwtUtil jwtUtil) {this.dataSource = dataSource;this.userDetailsService = userDetailsService;this.passwordEncoder = passwordEncoder;this.apiKeyAuthenticationProvider = apiKeyAuthenticationProvider;this.jwtAuthenticationProvider = jwtAuthenticationProvider;this.jwtUtil = jwtUtil;}@Beanpublic PasswordEncoder passwordEncoder() { /* ... */ return new BCryptPasswordEncoder(); }@Beanpublic UserDetailsService userDetailsService() { /* ... */ return new CustomUserDetailsService(sysUserMapper); }@Beanpublic PersistentTokenRepository persistentTokenRepository() { /* ... */ return tokenRepository; }@Beanpublic ProviderManager authenticationManager() {DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();daoProvider.setUserDetailsService(userDetailsService);daoProvider.setPasswordEncoder(passwordEncoder);// ProviderManager 現在包含 DaoAuthenticationProvider, ApiKeyAuthenticationProvider 和 JwtAuthenticationProviderreturn new ProviderManager(daoProvider, apiKeyAuthenticationProvider, jwtAuthenticationProvider);}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()) // <<-- HERE: 禁用CSRF防護.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // <<-- HERE: 設置為無狀態會話策略).authorizeHttpRequests(authorize -> authorize// 允許所有請求,因為我們現在是無狀態API,登錄獲取Token.requestMatchers("/api/auth/**", "/public/**", "/register", "/login").permitAll()// 不需要這些Web頁面的權限配置了,因為它們現在應該由前端路由控制// .requestMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN", "USER_MANAGE")// .requestMatchers("/user/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN", "USER_VIEW").requestMatchers("/api/v2/**").hasAuthority("API_KEY_AUTH").anyRequest().authenticated() // 其他所有 API 請求都需要認證 (JWT 或 API Key))// 移除了 formLogin 和 rememberMe, 因為現在是無狀態API.httpBasic(Customizer.withDefaults()) // 可以在測試階段保留HTTP Basic.addFilterBefore(new ApiKeyAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)// <<-- HERE: 將 JwtAuthenticationFilter 添加到 ApiKeyAuthenticationFilter 之后,UsernamePasswordAuthenticationFilter 之前// 但因為我們禁用了 Session,UsernamePasswordAuthenticationFilter 實際上不會被用到,可以考慮移除// 這里我們放在 ApiKeyAuthenticationFilter 之后,保證 JWT 認證在 API Key 認證之后嘗試.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), ApiKeyAuthenticationFilter.class);// TODO: 為JWT認證添加適當的異常處理器,例如 AuthenticationEntryPoint// .exceptionHandling(exception -> exception//     .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 稍后添加// )return http.build();}@Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher(); // 即使是 STATELESS,這個Bean本身沒有什么副作用,可以保留}
}

重要的更新點:

  1. JWT相關注入: JwtAuthenticationProviderJwtUtil被注入,并JwtAuthenticationProvider添加到ProviderManager中。
  2. 禁用CSRF和Session: csrf(csrf -> csrf.disable())sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 是實現無狀態的關鍵。
  3. 移除Session相關配置: formLogin()rememberMe()配置被移除,因為它們依賴于Session。
  4. JWT過濾器添加: JwtAuthenticationFilter通過 addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), ApiKeyAuthenticationFilter.class) 添加到過濾器鏈中,它將在ApiKeyAuthenticationFilter之前嘗試處理JWT認證。你可以自行調整順序。
  5. UsernamePasswordAuthenticationFilter的去留: 由于我們禁用了Session和表單登錄,UsernamePasswordAuthenticationFilter實際上不再具有作用。此處將其保留在addFilterBefore的參考中,但如果你不打算使用HTTP Basic或傳統的表單登錄,可以完全移除對它的引用,或者直接將其替換。對于純API,我們通常不會使用UsernamePasswordAuthenticationFilter
    • 更新: 為了清晰,我們將JWT過濾器放在所有認證過濾器之前,讓它優先處理Bearer Token。
    • UsernamePasswordAuthenticationFilter.class 如果不使用表單登錄,可以將其作為參考位置,或者使用更通用的過濾器,如 BasicAuthenticationFilter.class。這里,我們將API key認證放在它之前,JWT認證放在API key認證之前,形成優先順序。

4.7 改造登錄接口,返回JWT

我們需要創建一個新的登錄Controller,它接收用戶名和密碼,并在認證成功后返回JWT。

LoginApiController.java

package com.example.springsecuritystage1.controller;import com.example.springsecuritystage1.model.LoginRequest;
import com.example.springsecuritystage1.model.LoginResponse;
import com.example.springsecuritystage1.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;// 登錄請求體
class LoginRequest {private String username;private String password;// Getters and Setterspublic String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public String getPassword() { return password; }public void setPassword(String password) { this.password = password; }
}// 登錄響應體 (包含JWT)
class LoginResponse {private String token;private String type = "Bearer";private Long id;private String username;private String email; // 假設有private List<String> roles; // 假設有// Constructors, Getters, Setterspublic LoginResponse(String accessToken, Long id, String username, String email, List<String> roles) {this.token = accessToken;this.id = id;this.username = username;this.email = email;this.roles = roles;}public String getToken() { return token; }public void setToken(String token) { this.token = token; }public String getType() { return type; }public void setType(String type) { this.type = type; }public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public String getEmail() { return email; }public void setEmail(String email) { this.email = email; }public List<String> getRoles() { return roles; }public void setRoles(List<String> roles) { this.roles = roles; }
}@RestController
@RequestMapping("/api/auth")
public class LoginApiController {private final AuthenticationManager authenticationManager;private final JwtUtil jwtUtil;public LoginApiController(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {this.authenticationManager = authenticationManager;this.jwtUtil = jwtUtil;}@PostMapping("/login")public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));// 如果上面認證失敗,會拋出 AuthenticationException,不會走到這里SecurityContextHolder.getContext().setAuthentication(authentication);UserDetails userDetails = (UserDetails) authentication.getPrincipal();String jwt = jwtUtil.generateToken(userDetails);// 這里僅為了演示,id, email, roles可以從 userDetails 中提取或從數據庫查詢List<String> roles = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());return ResponseEntity.ok(new LoginResponse(jwt, null, userDetails.getUsername(), null, roles));}
}

4.8 認證失敗與權限不足的自定義處理

由于我們禁用了Session和表單登錄,Spring Security默認的重定向行為將不再適用。對于API,我們應該返回JSON格式的錯誤響應。

A. 未認證 (AuthenticationEntryPoint)
當用戶未提供憑證或憑證無效時,AuthenticationEntryPoint會被觸發。

package com.example.springsecuritystage1.security.handler;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import java.io.IOException;// 處理未認證的請求,返回401 Unauthorized
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)throws IOException, ServletException {System.out.println("Unauthorized error: " + authException.getMessage());response.setContentType("application/json");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getOutputStream().println("{ \"error\": \"" + authException.getMessage() + "\", \"code\": 401 }");}
}

B. 權限不足 (AccessDeniedHandler)
當用戶已認證但沒有所需權限時,AccessDeniedHandler會被觸發。

package com.example.springsecuritystage1.security.handler;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import java.io.IOException;// 處理權限不足的請求,返回403 Forbidden
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)throws IOException, ServletException {System.out.println("Access Denied error: " + accessDeniedException.getMessage());response.setContentType("application/json");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getOutputStream().println("{ \"error\": \"" + accessDeniedException.getMessage() + "\", \"code\": 403 }");}
}

C. 更新SecurityFilterChain,集成異常處理器

            .exceptionHandling(exception -> exception // <<-- HERE: 集成自定義異常處理器.authenticationEntryPoint(jwtAuthenticationEntryPoint) // 未認證.accessDeniedHandler(customAccessDeniedHandler) // 權限不足)

需要注入這兩個handler:

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;private final CustomAccessDeniedHandler customAccessDeniedHandler;public CustomSecurityConfig(// ... 其他注入JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,CustomAccessDeniedHandler customAccessDeniedHandler) {// ... 初始化this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;this.customAccessDeniedHandler = customAccessDeniedHandler;}

4.9 測試JWT認證流程

  1. 啟動應用。
  2. 獲取JWT: 使用Postman向 http://localhost:8080/api/auth/login 發送POST請求,Content-Type: application/json
    Body:
    {"username": "user","password": "password"
    }
    
    成功后,你應該會收到一個包含JWT的JSON響應,例如:
    {"token": "eyJhbGc...","type": "Bearer","username": "user","roles": ["ROLE_USER", "PRODUCT_READ", "USER_VIEW"]
    }
    
  3. 使用JWT訪問受保護資源:
    • 復制得到的token
    • http://localhost:8080/user/profile 發送GET請求,在請求頭中添加 Authorization: Bearer <你的JWT>
    • 你應該會收到 200 OK 響應,表示訪問成功。
  4. 訪問無權限資源:
    • 繼續使用同一個JWT(user用戶的),嘗試訪問 http://localhost:8080/admin/dashboard
    • 你應該收到 403 Forbidden 響應,內容為我們自定義的JSON錯誤。
  5. 訪問需要API Key的資源:
    • 嘗試使用JWT訪問 http://localhost:8080/api/v2/secret-data
    • 由于這個路徑需要API_KEY_AUTH權限,而JWT中可能沒有,所以還是會收到403 Forbidden
    • 此時,如果你在請求頭中同時提供正確的X-API-KEY,API Key認證會優先觸發,導致最終成功。這展示了多認證機制的協同工作。
  6. 無效/過期JWT:
    • 嘗試隨便修改JWT的某個字符,或者等待JWT過期(如果設置了短有效期)。
    • 再次發送請求,你應該收到 401 Unauthorized 響應。

5. JWT的安全性與挑戰

5.1 Token過期與刷新機制

  • 過期目的: JWT的exp聲明是其安全性的關鍵。短有效期可以限制令牌被盜用后的風險。
  • 刷新Token: 通常通過引入Refresh Token機制。
    • 用戶登錄后,同時獲取一個短期的Access Token(JWT)和一個長期的Refresh Token
    • Access Token用于訪問資源。
    • Access Token過期時,客戶端使用Refresh Token向認證服務器請求新的Access TokenRefresh Token
    • Refresh Token通常存儲在更安全的地方(如HttpOnly Cookie),并且只能使用一次,或者有被撤銷的機制。

5.2 JWT注銷/黑名單機制

JWT無法像Session一樣簡單地“注銷”。一旦簽發,只要簽名和有效期都沒問題,它就是有效的。
為了實現注銷功能或禁用被盜用的Token,可以采取:

  • 黑名單機制: 在服務器端維護一個已注銷/失效的JWT列表(通常存儲在Redis中,設置與JWT有效期相同的過期時間)。每次驗證JWT時,除了驗證簽名和有效期,還需檢查其是否在黑名單中。
  • 短有效期結合刷新: 這是更常見的做法。Access Token有效期設置很短,Refresh Token有效期長。當用戶登出時,只銷毀Refresh Token,Access Token自然很快過期。

5.3 密鑰管理

  • 生成與存儲: 簽名JWT的密鑰(secret)至關重要,必須是復雜、隨機且妥善保管的。生產環境應通過環境變量、配置文件或密鑰管理服務(如Vault)注入,絕不能硬編碼。
  • 輪換: 定期輪換密鑰是一種良好的安全實踐。

5.4 防止令牌盜用

  • Https: 始終通過HTTPS傳輸JWT,防止中間人攻擊竊取Token。
  • HttpOnly: 如果Token存儲在Cookie中,應設置為HttpOnly,防止XSS攻擊。
  • LocalStorage的風險: 將JWT存儲在LocalStorage中雖然方便,但易受XSS攻擊。

6. 常見陷阱與注意事項

  • 禁用CSRF與Session的警惕性: 只有當你確定你的應用不再依賴于Session,并且有其他安全措施時,才禁用它們。
  • JWT密鑰安全: 生產環境的JWT密鑰必須是強隨機字符串,且妥善保管。
  • JWT負載信息: 不要在JWT的Payload中存放敏感信息。JWT只是Base64編碼,不是加密。
  • JWT有效期: 根據業務需求合理設置JWT有效期。Access Token通常短,Refresh Token長。
  • 異常處理: 務必為AuthenticationEntryPointAccessDeniedHandler提供友好的JSON響應。
  • AuthenticationManager的構建: 確保ProviderManager包含了所有你需要的AuthenticationProvider

7. 階段總結

至此,你已經完成了Spring Security深度學習的第六階段!你現在已經能夠:

  • 理解JWT的核心概念、組成和工作原理。
  • 使用jjwt庫生成、解析和驗證JWT。
  • 在Spring Security中禁用Session和CSRF防護,構建一個無狀態的API認證系統。
  • 設計JwtAuthenticationTokenJwtAuthenticationProviderJwtAuthenticationFilter,并將其集成到Spring Security過濾器鏈中。
  • 改造登錄接口,使其返回JWT。
  • 定制API認證失敗和權限不足的JSON響應。

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

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

相關文章

無名信號量

include <myhead.h> oid *task( void *file_size)int file_size1*(int*)file_size;//打開源文件int fdopen("./hello",O_RDONLY);if(fd-1){perror("open error\n");return NULL;}//打開目標文件int fd1open("./world",O_WRONLY);if(fd1-1)…

免費CRM系統與Excel客戶管理的區別

很多中小企業在客戶管理初期&#xff0c;會選擇使用Excel表格進行客戶數據的整理與維護。但隨著業務規模擴大&#xff0c;客戶信息日益復雜&#xff0c;Excel逐漸暴露出諸多局限性。此時&#xff0c;免費CRM系統應運而生&#xff0c;成為企業客戶管理升級的重要選擇。本文將深入…

linux Nginx服務配置介紹,和配置流程

1、Nginx 配置介紹認識Nginx服務的主配置文件 nginx.confnginx的配置文件一般在 /usr/local/nginx/conf/下&#xff0c;然后直接vim nginx.com 即可編輯1.1 全局配置介紹全局配置位于主配置文件最頂部&#xff0c;作用于整個Nginx服務進程&#xff0c;影響服務的資源分配、運行…

文字識別接口-文字識別技術-ocr api

文字識別接口&#xff0c;顧名思義&#xff0c;就是一種將圖像文字或手寫文字轉換為可編輯文本的技術。文字識別接口&#xff0c;基于深度學習算法與自主ocr核心實現多種場景字符的高精度識別與結構化信息提取&#xff0c;現已被廣泛應用于銀行、醫療、財會、教育等多個領域。隨…

DeepSeek R1大模型微調實戰-llama-factory的模型下載與訓練

文章目錄概要1.下載模型2.llama factory 訓練模型2.1 模型微調2.2 模型評估2.3 模型對話2.4 導出模型3.硬件選擇概要 LLaMA Factory 是一個簡單易用且高效的大型語言模型訓練與微調平臺。通過它&#xff0c;用戶可以在無需編寫任何代碼的前提下&#xff0c;在本地完成上百種預…

C++ map和set

C參考文獻&#xff1a;cplusplus.com - The C Resources Network 目錄 一、序列式容器和關聯式容器 二、set系列 &#xff08;1&#xff09;set類的介紹 &#xff08;2&#xff09;set的構造和迭代器 &#xff08;3&#xff09;set的接口 1.insert?編輯 2.find和erase 3…

頭一次見問這么多kafka的問題

分享一篇粉絲朋友整理的面經&#xff0c;第一次遇見問那么多kafka的問題&#xff0c;看看他是怎么回答的。 先來看看 職位描述&#xff1a; 崗位職責&#xff1a; 負責基于 Go 的后端服務的設計、開發和維護&#xff1b;參與系統架構設計&#xff0c;確保系統的高可用性、高性能…

自底向上了解CPU的運算

文章目錄 引言 CPU如何實現邏輯運算 NMOS和PMOS 基于MOS管組合下的邏輯門運算 邏輯運算下運算的實現 ALU的誕生 CPU的誕生 關于二進制運算的研究 十進制轉二進制基礎換算 為什么負數要使用補碼進行表示 為什么反碼就能解決正負數相加問題,我們還需要用補碼來表示負數呢? 小數…

apache poi與Office Open XML關系

以下內容來自AI https://ecma-international.org/publications-and-standards/standards/ecma-376/ 官方規范 https://poi.apache.org/components/oxml4j/index.html java中針對Office Open XML的實現 Apache poi中各個組件 https://poi.apache.org/components/index.html …

S32K328上芯片內部RTC的使用和喚醒配置

1&#xff1a;RTC介紹 1.1 RTC基礎功能介紹 參考《S32K3xx Reference Manual》&#xff0c;S32K328芯片內部自帶RTC功能&#xff0c;并且支持從低功耗狀態下喚醒設備&#xff1b;1.2 RTC電源介紹 由以下三張圖可知 1&#xff1a;RTC由V11供電&#xff0c;V11依賴外部V15供電&am…

【Python】數據可視化之分類圖

目錄 條形圖 箱形圖 散點圖 分簇散點圖 小提琴 分簇小提琴 條形圖 條形圖是一種直觀的圖表形式&#xff0c;它通過不同長度的矩形條&#xff08;即“條形”&#xff09;來展示數值變量的中心趨勢估計值&#xff0c;其中每個矩形的高度直接對應于該組數據的某個中心量度&…

RabbitMQ模型詳解與常見問題

項目demo地址&#xff1a;https://github.com/tian-qingzhao/rabbitmq-demo 一、RabbitMQ組件概念 1.1 Server&#xff1a;接收客戶端的連接&#xff0c;實現AMQP實體服務。 1.2 Connection&#xff1a;連接 應用程序與Server的網絡連接&#xff0c;TCP連接。 1.3 Channel&…

網絡:相比于HTTP,HTTPS協議到底安全在哪?

網絡&#xff1a;相比于HTTP&#xff0c;HTTPS協議到底安全在哪&#xff1f; 我們知道HTTPS也是一種應用層協議&#xff0c;它在HTTP的基礎上有一層加密&#xff0c;因為HTTP的數據傳輸都是以明文方式傳輸的&#xff0c;所以加密主要是為了防止數據在傳輸的時候被篡改 今天我…

AI 基礎設施新范式,百度百舸 5.0 技術深度解析

本文整理自 2025 年 8 月 29 日百度云智大會 —— AI 算力平臺專題論壇&#xff0c;百度智能云 AI 計算首席科學家王雁鵬的同名主題演講。大家下午好&#xff01;昨天在主論壇&#xff0c;我們正式發布了百度百舸 AI 計算平臺 5.0&#xff0c;并展示了多項亮眼的性能數據。今天…

IO進程線程;多線程;線程互斥同步;互斥鎖;無名信號量;條件變量;0905

思維導圖多線程打印ABC運用無名面量 實現進程同步#include<myhead.h> //定義 無名信號量 sem_t sem1; sem_t sem2; sem_t sem3; //線程1 void* task1(void *arg) {while(1){sem_wait(&sem1);printf("A");fflush(stdout);sleep(1);sem_post(&sem2);} } …

固高 GTS-800 運動控制卡完全使用指南:從硬件部署到高階應用

固高 GTS-800 系列運動控制卡作為中端工業控制領域的標桿產品,以其 8-16 軸同步控制能力、豐富的插補功能和穩定的性能,廣泛應用于激光加工、PCB 制造、精密裝配等自動化設備中。本文將系統講解 GTS-800 的硬件架構、開發環境搭建、核心功能實現及工程實踐技巧,幫助工程師快…

STM32F103_Bootloader程序開發15 - 從Keil到vscode + EIDE + GCC的遷移實踐

導言 STM32 - Embedded IDE - GCC - 如何在工程中生成.bin格式固件 STM32 - Embedded IDE - GCC - 使用 GCC 鏈接腳本限制 Flash 區域 STM32 - Embedded IDE - GCC - 如何在工程中定義一段 NoInit RAM 內存 STM32 - Embedded IDE - GCC - 如何將編譯得到的.bin固件添加CRC32校驗…

HTTP協議——理解相關概念、模擬實現瀏覽器訪問自定義服務器

文章目錄HTTP協議理解相關概念HTTP相關背景知識認識URLHTTP協議在網絡通信的宏觀認識urlencode & urldecodeHTTP請求和應答的格式模擬實現瀏覽器訪問自定義服務器關于http requesthttp request的請求行——URI使用瀏覽器完成靜態資源的訪問常用的報頭屬性http response狀態…

【服務器】英偉達M40顯卡風冷方案心得

在之前的博文中&#xff0c;博主說到最近準備自己組裝一臺服務器&#xff0c;主要用于有限元仿真&#xff0c;其次兼顧一部分AI機器學習的工作&#xff0c;于是博主就入手了一張英偉達Tesla M40的12G顯卡GPU。本來博主也糾結過是買M40還是M60&#xff0c;后來在網上看到說M60看…

Java中的鎖升級機制

目錄 核心思想 Java對象頭&#xff08;Object Header&#xff09;與Mark Word 鎖升級的詳細步驟 1. 無鎖&#xff08;No Lock&#xff09; 2. 偏向鎖&#xff08;Biased Locking&#xff09; 3. 輕量級鎖&#xff08;Lightweight Lock&#xff09; 4. 重量級鎖&#xff…