Spring Boot Security:实现JWT过滤器对特定URL路径的精准控制

Spring Boot Security:实现JWT过滤器对特定URL路径的精准控制

本文详细介绍了如何在spring Boot Security框架中,精确配置JWT(json Web Token)过滤器,使其仅作用于指定的URL路径,而非全局拦截所有请求。通过继承AbstractAuthenticationProcessingFilter并结合RequestMatcher接口,您可以灵活定义需要JWT认证的API端点,从而优化安全策略,提升应用程序的性能与安全性。

1. 问题背景与传统做法的局限性

spring boot应用中,当我们需要为API接口添加JWT认证时,通常会自定义一个JWT认证过滤器,并使用httpSecurity.addFilterBefore()方法将其添加到spring security的过滤器链中。例如:

@Override public void configure(HttpSecurity http) throws Exception {     http.addFilterBefore(customJwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);     // ... 其他配置 }

这种做法虽然简单,但存在一个问题:customJwtAuthenticationFilter会拦截所有进入Spring Security过滤器的请求。如果我们的应用既有需要认证的API(如/api/**),也有公开访问的页面或接口(如/login, /, /public/**),那么即使是公开接口,也会被JWT过滤器尝试处理,这可能导致不必要的性能开销或逻辑复杂性。

理想情况下,我们希望JWT过滤器只对那些明确需要JWT认证的路径(例如,所有以/api/开头的路径)生效,而对其他路径则直接放行或交由其他过滤器处理。

2. 解决方案:利用 AbstractAuthenticationProcessingFilter 和 RequestMatcher

Spring Security提供了AbstractAuthenticationProcessingFilter,这是一个专门用于处理特定认证流程的抽象基类。它允许我们通过传入一个RequestMatcher实例来精确控制过滤器所处理的请求。当请求的URL与RequestMatcher匹配时,AbstractAuthenticationProcessingFilter才会尝试执行认证逻辑。

2.1 核心组件解析

  • AbstractAuthenticationProcessingFilter: 这是Spring Security提供的一个抽象过滤器,设计用于处理特定类型的认证请求。它内部持有一个RequestMatcher,只有当请求与该RequestMatcher匹配时,才会调用其attemptAuthentication()方法来执行认证逻辑。
  • RequestMatcher: 这是一个接口,用于判断给定的HttpServletRequest是否匹配某种条件。Spring Security提供了多种实现,如AntPathRequestMatcher(基于Ant风格路径匹配)、RegexRequestMatcher(基于正则表达式)、OrRequestMatcher(多个RequestMatcher中的任意一个匹配即可)等。

2.2 实现步骤

步骤一:修改 CustomJwtAuthenticationFilter

让你的JWT认证过滤器继承AbstractAuthenticationProcessingFilter,并在构造函数中接收一个RequestMatcher实例。同时,你需要重写attemptAuthentication()方法,将你原有的JWT解析和认证逻辑放入其中。

import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; 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;  // 假设你有一个JwtTokenProvider来处理JWT的生成和验证 // import com.yourpackage.security.JwtTokenProvider;  public class CustomJwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {      private final UserDetailsService userDetailsService;     // private final JwtTokenProvider jwtTokenProvider; // 假设你有一个JWT工具类      // 构造函数:传入RequestMatcher和AuthenticationManager     public CustomJwtAuthenticationFilter(             RequestMatcher requiresAuthenticationRequestMatcher,             AuthenticationManager authenticationManager,             UserDetailsService userDetailsService             /*, JwtTokenProvider jwtTokenProvider */) {         super(requiresAuthenticationRequestMatcher); // 指定哪些请求需要此过滤器处理         setAuthenticationManager(authenticationManager); // 设置认证管理器         this.userDetailsService = userDetailsService;         // this.jwtTokenProvider = jwtTokenProvider;     }      @Override     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)             throws AuthenticationException, IOException, ServletException {         // 1. 从请求头中提取JWT令牌         String authorizationHeader = request.getHeader("Authorization");          if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {             // 如果没有Bearer Token,或者格式不正确,则抛出认证异常             // AbstractAuthenticationProcessingFilter 会捕获此异常并调用 AuthenticationFailureHandler             throw new BadCredentialsException("Missing or invalid Authorization header");         }          String jwtToken = authorizationHeader.substring(7); // 移除 "Bearer " 前缀          // 2. 验证JWT令牌并获取用户信息         // 实际项目中,这里会调用你的JwtTokenProvider来验证令牌并解析出用户名         // 示例:         // if (!jwtTokenProvider.validateToken(jwtToken)) {         //     throw new BadCredentialsException("Invalid JWT token");         // }         // String username = jwtTokenProvider.getUsernameFromToken(jwtToken);          // 简化示例,直接假设从JWT中解析出用户名 "testuser"         String username = "testuser"; // 实际应从JWT中解析          // 3. 根据用户名加载用户详情         UserDetails userDetails = userDetailsService.loadUserByUsername(username);          // 4. 创建一个认证令牌并交由AuthenticationManager进行认证         // 注意:这里通常不需要再次调用authenticationManager.authenticate(),         // 因为JWT本身就是一种认证凭证。我们直接创建一个已认证的Authentication对象。         // 如果你的JWT验证逻辑非常复杂,需要AuthenticationManager的Provider来处理,         // 则可以构造一个UsernamePasswordAuthenticationToken或其他Token,然后调用getAuthenticationManager().authenticate()         // 但对于多数JWT场景,直接返回一个已认证的Token即可。         UsernamePasswordAuthenticationToken authenticationToken =                 new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());          // 将请求详情设置到Authentication对象中,以便后续的WebAuthenticationDetailsSource使用         authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));          return authenticationToken; // 返回已认证的Authentication对象     }      // 可以在这里重写 successfulAuthentication 和 unsuccessfulAuthentication     // 以处理认证成功或失败后的逻辑,例如记录日志、设置响应头等。     // @Override     // protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {     //     SecurityContextHolder.getContext().setAuthentication(authResult);     //     chain.doFilter(request, response);     // }      // @Override     // protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {     //     // 可以在这里自定义失败响应     //     super.unsuccessfulAuthentication(request, response, failed);     // } }

步骤二:在 WebSecurityConfigurerAdapter 中配置过滤器

在你的安全配置类中,将CustomJwtAuthenticationFilter声明为一个Spring Bean,并在configure(HttpSecurity http)方法中将其添加到过滤器链。关键在于创建CustomJwtAuthenticationFilter实例时,传入正确的RequestMatcher。

import org.springframework.beans.factory.annotation.Autowired; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;  import java.util.Arrays; import java.util.List; import java.util.stream.Collectors;  @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {      @Autowired     private UserDetailsService userDetailsService; // 你的UserDetailsService实现     // @Autowired     // private JwtTokenProvider jwtTokenProvider; // 你的JWT工具类      // 假设你有一个JWT认证入口点,处理未认证的请求     // @Autowired     // private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());     }      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }      @Bean     @Override     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }      // 声明 CustomJwtAuthenticationFilter 为 Spring Bean     @Bean     public CustomJwtAuthenticationFilter customJwtAuthenticationFilter() throws Exception {         // 定义需要JWT过滤器处理的URL模式         // 示例1: 只匹配 /api/**         RequestMatcher protectedUrlMatcher = new AntPathRequestMatcher("/api/**");          // 示例2: 匹配多个URL模式 (如 /api/users/** 和 /api/products/**)         // List<String> protectedPaths = Arrays.asList("/api/users/**", "/api/products/**");         // RequestMatcher protectedUrlMatcher = new OrRequestMatcher(         //     protectedPaths.stream()         //                   .map(AntPathRequestMatcher::new)         //                   .collect(Collectors.toList())         // );          // 实例化 CustomJwtAuthenticationFilter,传入RequestMatcher、AuthenticationManager和UserDetailsService         // 注意:authenticationManagerBean() 在这里直接调用会抛出异常,因为它需要Spring上下文初始化。         // 正确的做法是将其作为参数注入到 @Bean 方法中,或者在 configure(HttpSecurity) 中获取。         // 这里通过在 @Bean 方法签名中添加 AuthenticationManager authenticationManager 来注入         return new CustomJwtAuthenticationFilter(                 protectedUrlMatcher,                 authenticationManagerBean(), // 获取AuthenticationManager实例                 userDetailsService                 /*, jwtTokenProvider */);     }       @Override     protected void configure(HttpSecurity http) throws Exception {         http.csrf().disable() // 禁用CSRF             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT通常是无状态的             .and()             .exceptionHandling()                 // .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 处理未认证的请求                 // .AccessDeniedPage("/403") // 处理无权限的请求             .and()             .authorizeRequests()                 // 确保被JWT过滤器处理的路径需要认证                 .antMatchers("/api/**").authenticated()                 // 其他路径可以公开访问                 .antMatchers("/login", "/", "/public/**").permitAll()                 .anyRequest().authenticated() // 默认所有其他请求都需要认证             .and()             // 将自定义的JWT过滤器添加到UsernamePasswordAuthenticationFilter之前             .addFilterBefore(customJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);             // .formLogin() // 如果你还需要表单登录,可以继续配置             //     .loginPage("/login")             //     .defaultSuccessUrl("/users")             //     .failureUrl("/login?error=true")             //     .permitAll()             // .and()             // .logout()             //     .logoutSuccessUrl("/")             //     .permitAll();     } }

3. 注意事项与总结

  1. attemptAuthentication() 方法的实现:这是你JWT认证逻辑的核心。你需要从请求中提取JWT,验证其有效性,并根据令牌中的信息(如用户ID或用户名)加载用户详情(UserDetails)。最后,创建一个已认证的Authentication对象并返回。
  2. AuthenticationManager 的注入:AbstractAuthenticationProcessingFilter 需要一个AuthenticationManager来处理认证。在@Bean方法中,Spring会自动注入AuthenticationManager实例。
  3. authorizeRequests() 的配合:即使你的CustomJwtAuthenticationFilter已经通过RequestMatcher过滤了请求,HttpSecurity.authorizeRequests()中的antMatchers(“/api/**”).authenticated()仍然是必不可少的。前者负责“尝试”认证(如果请求匹配),后者负责“强制”认证(如果请求需要认证但未认证)。两者协同工作,确保只有通过JWT认证的请求才能访问/api/**路径。
  4. 错误处理:AbstractAuthenticationProcessingFilter在认证失败时会抛出AuthenticationException。你可以通过设置AuthenticationFailureHandler来自定义认证失败后的行为(例如返回特定的JSON错误响应)。
  5. 会话管理:对于JWT认证,通常会将会话管理设置为STATELESS,因为JWT本身包含了认证信息,服务器无需维护会话状态。
  6. 依赖注入:确保你的CustomJwtAuthenticationFilter所依赖的其他服务(如UserDetailsService, JwtTokenProvider)能够正确地通过Spring的依赖注入机制获取到。

通过以上配置,你的JWT过滤器将只会对/api/**路径下的请求进行处理,而其他路径(如/login、/等)将不再经过JWT过滤器的认证逻辑,从而实现更精准、高效的安全控制。

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