本教程詳細講解如何在spring Boot Security中,精確控制JWT(json Web Token)過濾器的應用范圍,使其僅作用于指定的URL模式,而非全局生效。通過繼承AbstractAuthenticationProcessingFilter并結合RequestMatcher接口,開發者可以靈活定義哪些請求路徑需要JWT認證,從而優化安全策略,避免不必要的性能開銷,并增強應用的模塊化安全性。文章將提供詳細的代碼示例和配置步驟,幫助讀者實現定制化的安全過濾邏輯。
在spring security中,我們經常需要自定義過濾器來處理特定的認證或授權邏輯,例如jwt認證。然而,默認情況下,通過httpsecurity.addfilterbefore()或addfilterat()添加的過濾器會作用于所有進來的http請求。對于jwt認證而言,通常我們只希望它對受保護的api路徑(例如/api/**)生效,而對靜態資源、登錄頁面或公共接口則無需進行jwt驗證。本文將介紹如何利用spring security提供的abstractauthenticationprocessingfilter和requestmatcher接口,實現jwt過濾器的精確控制。
挑戰:全局過濾器與局部需求
當我們將一個自定義JWT過濾器(例如CustomJwtAuthenticationFilter)通過以下方式添加到安全鏈中時:
http.addFilterBefore(customJwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
這個customJwtAuthenticationFilter將會在UsernamePasswordAuthenticationFilter之前,對所有進入應用的請求進行處理。這意味著即使是訪問/login、/或任何非API路徑,該過濾器也會被觸發,這不僅可能導致不必要的性能開銷,還可能在某些情況下拋出異常(例如,嘗試從沒有JWT的請求頭中解析令牌)。
解決方案:AbstractAuthenticationProcessingFilter與RequestMatcher
Spring Security提供了一個抽象類AbstractAuthenticationProcessingFilter,它專門用于處理基于請求匹配的認證流程。這個類的核心在于其構造函數可以接收一個RequestMatcher對象。當一個請求到達時,AbstractAuthenticationProcessingFilter會首先調用其內部的RequestMatcher的matches()方法。只有當matches()方法返回true時,過濾器才會繼續執行其認證邏輯(即調用attemptAuthentication()方法);否則,它會直接跳過認證過程,將請求傳遞給安全鏈中的下一個過濾器。
RequestMatcher是一個接口,它定義了如何根據HttpServletRequest來判斷一個請求是否匹配特定條件。Spring Security提供了多種RequestMatcher的實現,其中最常用的是:
- AntPathRequestMatcher:基于Ant風格路徑模式匹配URL。
- OrRequestMatcher:將多個RequestMatcher組合,只要有一個匹配就返回true。
- AndRequestMatcher:將多個RequestMatcher組合,只有所有都匹配才返回true。
- NegatedRequestMatcher:對另一個RequestMatcher的結果取反。
實現步驟
我們將通過以下步驟實現JWT過濾器的精確控制:
1. 改造 CustomJwtAuthenticationFilter
讓你的JWT認證過濾器繼承AbstractAuthenticationProcessingFilter,并在構造函數中接收RequestMatcher和AuthenticationManager。
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定義JWT認證過濾器,僅對匹配特定RequestMatcher的請求進行處理。 */ public class CustomJwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * 構造函數。 * @param requiresAuthenticationRequestMatcher 定義哪些請求需要此過濾器處理的RequestMatcher * @param authenticationManager 認證管理器,用于執行認證邏輯 */ public CustomJwtAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) { super(requiresAuthenticationRequestMatcher); // 將RequestMatcher傳遞給父類 setAuthenticationManager(authenticationManager); // 設置認證管理器 } /** * 實現JWT認證的核心邏輯。 * 只有當RequestMatcher匹配時,此方法才會被調用。 */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { // 在這里實現你的JWT解析和認證邏輯 // 例如:從請求頭中獲取JWT令牌 String authorizationHeader = request.getHeader("Authorization"); if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { // 如果沒有Bearer Token,拋出認證異常,或返回null讓后續認證機制處理 throw new AuthenticationException("Missing or invalid JWT token in Authorization header") {}; } String jwtToken = authorizationHeader.substring(7); // 提取JWT字符串 // TODO: 根據你的JWT庫和業務邏輯驗證jwtToken,并構建一個Authentication對象 // 例如: // JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(jwtToken); // return getAuthenticationManager().authenticate(authenticationToken); // 委托給AuthenticationManager進行認證 // 示例:此處僅為演示,實際應替換為你的JWT驗證邏輯 System.out.println("Processing JWT for path: " + request.getRequestURI()); // 假設成功驗證并返回一個Authentication對象 // return new UsernamePasswordAuthenticationToken("user", null, Collections.emptyList()); throw new UnsupportedOperationException("JWT認證邏輯待實現,請替換為實際的令牌驗證和用戶身份構建。"); } // 可選:重寫successfulAuthentication和unsuccessfulAuthentication方法來處理認證成功或失敗后的邏輯 // @Override // protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // super.successfulAuthentication(request, response, chain, authResult); // // 認證成功后繼續過濾器鏈 // chain.doFilter(request, response); // } // @Override // protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { // // 認證失敗處理,例如返回401 Unauthorized // response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // response.getWriter().write("Authentication Failed: " + failed.getMessage()); // } }
2. 定義 RequestMatcher
針對“只過濾/api/**路徑”的需求,我們可以使用AntPathRequestMatcher。
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; // 簡單匹配單個路徑模式 // RequestMatcher apiRequestMatcher = new AntPathRequestMatcher("/api/**"); // 如果需要匹配多個路徑模式,可以使用OrRequestMatcher // RequestMatcher multiPathMatcher = new OrRequestMatcher( // new AntPathRequestMatcher("/api/v1/**"), // new AntPathRequestMatcher("/secure/**") // );
3. 配置 Spring Security
在你的安全配置類(通常是繼承WebSecurityConfigurerAdapter或使用SecurityFilterChain)中,將改造后的CustomJwtAuthenticationFilter作為Bean注入,并添加到安全鏈中。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // 假設你有一個JwtAuthenticationEntryPoint處理認證失敗的入口點 // @Autowired // private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; // 假設你有一個UserDetailsService用于加載用戶詳情(如果JWT認證需要) // @Autowired // private UserDetailsService userDetailsService; /** * 配置HTTP安全策略。 */ @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // 禁用CSRF,因為JWT是無狀態的 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 設置會話管理為無狀態 .and() // .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and() // 配置認證入口點處理未認證請求 .authorizeRequests() .antMatchers("/api/**").authenticated() // 明確指定 /api/** 路徑需要認證 .anyRequest().permitAll() // 其他所有請求都允許訪問 .and() // 將我們定制的JWT過濾器添加到UsernamePasswordAuthenticationFilter之前 .addFilterBefore(customJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } /** * 將CustomJwtAuthenticationFilter注冊為Spring Bean。 * 注意:這里需要捕獲AuthenticationManagerBean()拋出的異常。