JwtAuthFilter对已放行端点仍生效 引发认证循环问题求助
解决Spring Security 6中JWT过滤器拦截已放行端点的问题
问题场景
使用Spring Boot 3.1.4 + Spring Security 6.1.4,已通过authorizeHttpRequests配置/、/login、/register、/error/**等端点无需认证,但添加自定义JwtAuthFilter后,该过滤器会对所有请求生效,包括上述已放行的端点。例如访问/error页面时,过滤器仍会尝试进行JWT认证,引发新的403错误,最终导致认证循环。
原因分析
Spring Security的过滤器链执行顺序中,通过addFilterAfter添加的自定义过滤器会在UsernamePasswordAuthenticationFilter之后执行,且不受后续授权规则的提前拦截。授权规则(permitAll)是在过滤器链的FilterSecurityInterceptor阶段才会判断是否放行请求,而JwtAuthFilter在这之前就已经处理所有请求,因此会对已配置为放行的端点也执行认证逻辑。
解决方案
方案一:在JwtAuthFilter中跳过放行路径
直接在过滤器内部判断请求路径,若属于无需认证的范围,则直接放行,不执行JWT处理逻辑。
修改JwtAuthFilter代码:
@Component @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserProfileRepository userProfileRepository; private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { // 跳过无需认证的路径 String requestPath = request.getRequestURI(); if (pathMatcher.match("/", requestPath) || pathMatcher.match("/login", requestPath) || pathMatcher.match("/register", requestPath) || pathMatcher.match("/error/**", requestPath)) { filterChain.doFilter(request, response); return; } // 原有的JWT处理逻辑 final Cookie[] cookies = request.getCookies(); if (cookies == null || cookies.length == 0) { filterChain.doFilter(request, response); return; } Optional<Cookie> authorizationCookie = Arrays.stream(cookies) .filter(cookie -> cookie.getName().equals("Authorization")) .findFirst(); if (authorizationCookie.isEmpty()) { filterChain.doFilter(request, response); return; } String token = authorizationCookie.get().getValue(); String mail = jwtService.extractMail(token); if (mail == null) { filterChain.doFilter(request, response); return; } SecurityContext context = SecurityContextHolder.getContext(); if (context.getAuthentication() != null) { filterChain.doFilter(request, response); return; } RegisteredUser user = userProfileRepository.findByMail(Email.of(mail)); if (user == null) { filterChain.doFilter(request, response); return; } if (jwtService.isTokenValid(token, user)) { var authenticationToken = new UsernamePasswordAuthenticationToken( user, null, user.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); context.setAuthentication(authenticationToken); } filterChain.doFilter(request, response); } }
方案二:在SecurityFilterChain中限定过滤器作用路径
利用Spring Security提供的带RequestMatcher参数的addFilterAfter重载方法,只让JwtAuthFilter作用于需要认证的端点,避免处理已放行路径。
修改WebSecurityConfig代码:
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final JwtAuthFilter jwtAuthFilter; private final UserProfileRepository userProfileRepository; private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(auth -> { auth.requestMatchers("/", "/login", "/register", "/error/**").permitAll(); auth.anyRequest().authenticated(); // 其余请求需认证 }) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationProvider(authenticationProvider()) // 仅对非放行路径应用JWT过滤器 .addFilterAfter(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class, request -> !pathMatcher.match("/", request.getRequestURI()) && !pathMatcher.match("/login", request.getRequestURI()) && !pathMatcher.match("/register", request.getRequestURI()) && !pathMatcher.match("/error/**", request.getRequestURI())) .build(); } // 其余Bean定义保持不变... }
更优雅的路径匹配方式
可以将放行路径的匹配逻辑封装为RequestMatcher Bean,便于复用和维护:
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final JwtAuthFilter jwtAuthFilter; private final UserProfileRepository userProfileRepository; @Bean public RequestMatcher unsecuredPathsMatcher() { return new OrRequestMatcher( new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/login"), new AntPathRequestMatcher("/register"), new AntPathRequestMatcher("/error/**") ); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(auth -> { auth.requestMatchers(unsecuredPathsMatcher()).permitAll(); auth.anyRequest().authenticated(); }) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationProvider(authenticationProvider()) // 对非放行路径应用JWT过滤器 .addFilterAfter(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class, new NegatedRequestMatcher(unsecuredPathsMatcher())) .build(); } // 其余Bean定义保持不变... }
方案对比
- 方案一:实现简单,适合快速修复,但路径规则分散在过滤器和配置类中,维护成本较高。
- 方案二:符合Spring Security的设计思想,将路径匹配逻辑集中在配置类中,便于统一管理和扩展,推荐使用。
内容的提问来源于stack exchange,提问作者Kerem
相关产品推荐
相关产品推荐

