You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 21:47:09