本文旨在详细阐述如何在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访问控制。