Spring Boot Security中JWT过滤器针对特定URL模式的精准应用

Spring Boot Security中JWT过滤器针对特定URL模式的精准应用

本文旨在详细阐述如何在spring Boot Security框架中,利用AbstractAuthenticationProcessingFilter和RequestMatcher机制,实现JWT认证过滤器只应用于特定URL模式(如/api/**)而非所有请求。通过构建自定义的请求匹配器并将其集成到过滤器中,开发者可以实现更精细化的安全控制,有效避免不必要的认证处理,从而优化系统性能并提升安全性。

spring boot应用中集成jwt(json web Token)进行认证是常见的做法。然而,默认情况下,如果我们将自定义的jwt认证过滤器直接添加到spring security的过滤器链中,它可能会拦截所有传入的http请求。在某些场景下,我们可能只希望对特定路径(例如restful api接口,通常以/api/开头)进行jwt认证,而让其他路径(如静态资源、登录页面、公共接口等)不受jwt过滤器的影响。本文将介绍如何通过扩展abstractauthenticationprocessingfilter并结合requestmatcher接口来实现这一目标。

1. 理解核心组件:AbstractAuthenticationProcessingFilter与RequestMatcher

spring security提供了AbstractAuthenticationProcessingFilter作为处理特定认证请求的抽象基类。这个过滤器在内部使用一个RequestMatcher来决定当前请求是否需要进行认证处理。如果RequestMatcher的matches()方法返回true,则过滤器会尝试处理认证;如果返回false,则直接放行,不进行认证处理。这正是我们实现特定URL过滤的关键。

2. 构建自定义JWT认证过滤器

我们的自定义JWT认证过滤器CustomJwtAuthenticationFilter需要继承AbstractAuthenticationProcessingFilter。关键在于其构造函数,它必须接收一个RequestMatcher实例。

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 jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException;  public class CustomJwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {      // 假设您有一个JWT解析和验证的服务     private final JwtTokenProvider jwtTokenProvider;      public CustomJwtAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, JwtTokenProvider jwtTokenProvider, AuthenticationManager authenticationManager) {         super(requiresAuthenticationRequestMatcher); // 将RequestMatcher传递给父类         this.jwtTokenProvider = jwtTokenProvider;         setAuthenticationManager(authenticationManager); // 设置认证管理器     }      @Override     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)             throws AuthenticationException, IOException, ServletException {         // 从请求头中获取JWT Token         String token = jwtTokenProvider.resolveToken(request);          if (token != null && jwtTokenProvider.validateToken(token)) {             // 如果Token有效,则构建一个认证对象             // 这里的AuthenticationToken需要根据您的JWT实现来定义             // 例如,可以是一个包含用户信息的UsernamePasswordAuthenticationToken             Authentication authentication = jwtTokenProvider.getAuthentication(token);             return getAuthenticationManager().authenticate(authentication); // 交给AuthenticationManager进行认证         }         // 如果没有Token或Token无效,则抛出认证异常         throw new AuthenticationException("JWT Token is missing or invalid") {};     }      @Override     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,                                             Authentication authResult) throws IOException, ServletException {         // 认证成功后,将认证信息设置到SecurityContext中,并继续过滤器链         // SecurityContextHolder.getContext().setAuthentication(authResult); // AbstractAuthenticationProcessingFilter 内部会处理         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());     } }

注意:上述代码中的JwtTokenProvider是一个假设的服务,用于解析和验证JWT。getAuthentication()方法应根据您的JWT结构返回一个Authentication对象,通常是UsernamePasswordAuthenticationToken。

3. 实现自定义的RequestMatcher

为了让过滤器只作用于/api/**路径,我们需要创建一个RequestMatcher的实现类,其matches()方法在请求路径匹配/api/**时返回true。

import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import jakarta.servlet.http.HttpServletRequest;  public class ApiPathRequestMatcher implements RequestMatcher {      private final AntPathRequestMatcher apiPathMatcher;      public ApiPathRequestMatcher(String pattern) {         this.apiPathMatcher = new AntPathRequestMatcher(pattern);     }      @Override     public boolean matches(HttpServletRequest request) {         // 如果请求路径匹配指定的模式,则返回true,表示需要进行认证处理         return apiPathMatcher.matches(request);     } }

这里我们使用了Spring Security内置的AntPathRequestMatcher来方便地匹配Ant风格的路径模式。

4. 在Spring Security配置中集成过滤器

最后一步是将CustomJwtAuthenticationFilter和ApiPathRequestMatcher集成到Spring Security的配置中。

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.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; // 或者其他认证入口点  @Configuration @EnableWebSecurity public class WebSecurityConfig {      private final JwtTokenProvider jwtTokenProvider; // 您的JWT提供者服务     private final BasicAuthenticationEntryPoint jwtAuthenticationEntryPoint; // 认证入口点,用于未认证访问时的处理      public WebSecurityConfig(JwtTokenProvider jwtTokenProvider, BasicAuthenticationEntryPoint jwtAuthenticationEntryPoint) {         this.jwtTokenProvider = jwtTokenProvider;         this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;     }      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }      @Bean     public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {         return authenticationConfiguration.getAuthenticationManager();     }      @Bean     public CustomJwtAuthenticationFilter customJwtAuthenticationFilter(AuthenticationManager authenticationManager) {         // 创建只匹配/api/**路径的请求匹配器         ApiPathRequestMatcher apiMatcher = new ApiPathRequestMatcher("/api/**");         return new CustomJwtAuthenticationFilter(apiMatcher, jwtTokenProvider, authenticationManager);     }      @Bean     public SecurityFilterChain filterChain(HttpSecurity http, CustomJwtAuthenticationFilter customJwtAuthenticationFilter) throws Exception {         http.csrf().disable() // 禁用CSRF             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT通常是无状态的             .and()             .exceptionHandling()                 .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 未认证访问时的处理                 .AccessDeniedPage("/403") // 访问被拒绝时的页面             .and()             .authorizeRequests()                 // /api/** 路径将由我们的CustomJwtAuthenticationFilter进行认证,                 // 但这里仍然需要配置其授权规则(例如,认证后才能访问)                 .antMatchers("/api/**").authenticated()                 // 其他路径允许所有访问                 .anyRequest().permitAll()             .and()             // 如果您的应用同时有表单登录,可以保留以下配置             .formLogin()                 .loginPage("/login")                 .defaultSuccessUrl("/users")                 .failureUrl("/login?error=true")                 .permitAll()             .and()             .logout()                 .logoutSuccessUrl("/")                 .permitAll()             .and()             // 将我们的自定义JWT过滤器添加到UsernamePasswordAuthenticationFilter之前             // 这样JWT认证会在Spring Security的默认表单认证之前执行             .addFilterBefore(customJwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);          return http.build();     } }

5. 注意事项与总结

  • 过滤器顺序:通过addFilterBefore(customJwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class),我们确保了自定义的JWT过滤器在Spring Security默认的用户名密码认证过滤器之前执行。这对于API认证场景是合适的,因为我们希望首先尝试JWT认证。
  • authorizeRequests()与自定义过滤器:即使CustomJwtAuthenticationFilter负责认证/api/**路径,authorizeRequests().antMatchers(“/api/**”).authenticated()仍然是必要的。前者处理“认证”过程(验证凭据并识别用户),后者处理“授权”过程(认证后用户是否有权限访问该资源)。如果JWT认证成功,用户将被认证,然后authorizeRequests()会根据其认证状态(authenticated())允许访问。
  • 无状态会话:JWT认证通常是无状态的,因此将sessionCreationPolicy(SessionCreationPolicy.STATELESS)设置为无状态是最佳实践,这会告诉Spring Security不要创建或使用HTTP会话来存储安全上下文。
  • 异常处理:authenticationEntryPoint用于处理未认证用户尝试访问受保护资源的情况。accessDeniedPage用于处理已认证用户但无权访问特定资源的情况。
  • JwtTokenProvider和AuthenticationManager:在实际应用中,您需要实现JwtTokenProvider来处理JWT的生成、解析和验证,并配置AuthenticationManager来处理认证逻辑(例如,通过UserDetailsService加载用户详情)。

通过上述配置,您的Spring Boot应用将能够精确地控制JWT认证过滤器的作用范围,仅对/api/**等指定路径进行JWT认证,从而实现更高效、更安全的API访问控制。

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享