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

同一API路径下配置多种认证机制的问题及异常排查

问题描述

我们的服务需接收两类请求:集群内部请求与外部请求。内部请求通过JWT令牌验证,外部请求仅需校验请求头即可。希望借助Spring OAuth2默认机制实现自动校验逻辑:若存在令牌则校验令牌,无令牌则校验请求头(顺序可调整)。核心难点是两类请求的API路径完全相同。

目前尚未找到可行方案,且自定义的CustomOAuth2Filter中调用getAuthenticationManager()时抛出StackOverflowException异常。


主配置类代码

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {

    @Bean
    public CustomOAuth2Filter customOAuth2Filter(AuthenticationManager authenticationManager) {
        CustomOAuth2Filter filter = new CustomOAuth2Filter(authenticationManager);
        return filter;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().permitAll() // 默认允许未认证访问
                .and()
                .addFilterBefore(customOAuth2Filter(authenticationManager), BasicAuthenticationFilter.class) // 在基础认证过滤器前添加自定义过滤器
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName("groups");
        grantedAuthoritiesConverter.setAuthorityPrefix("");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}

自定义过滤器代码

@Component
public class CustomOAuth2Filter extends AbstractAuthenticationProcessingFilter {

    public CustomOAuth2Filter(AuthenticationManager authenticationManager) {
        super("/path/**");
        setAuthenticationManager(authenticationManager);
    }

    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        String authHeader = request.getHeader("Authorization");
        return authHeader != null && authHeader.startsWith("Bearer ");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        extractUserInfo(request);
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            return SecurityContextHolder.getContext().getAuthentication();
        }

        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token);
            return getAuthenticationManager().authenticate(authenticationToken);
        }
        return null; // 无令牌时返回null,继续不认证流程
    }

    @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 {
        SecurityContextHolder.clearContext();
        // chain.doFilter(request, response); // 不认证继续流程
    }


    protected void extractUserInfo(HttpServletRequest request) throws ServletException, IOException {
        String username = extractHeader(request, "user");
        String roleStr = extractHeader(request, "roles");
        if (username == null || roleStr == null || roleStr.isBlank()) {
            return;
        }

        List<String> roles = Arrays.asList(roleStr.split(","));
        List<GrantedAuthority> grantedAuthorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        MyUser user = new MyUser(username, "", grantedAuthorities);

        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(auth);
    }

    private String extractHeader(HttpServletRequest request, String extractValue) {
        return request.getHeader(extractValue);
    }
}

解决方案

1. 修复StackOverflowException异常

当前配置存在循环创建实例的问题:customOAuth2Filter已通过@Bean声明,却在filterChain方法中再次调用customOAuth2Filter(authenticationManager)创建新实例,导致Spring循环依赖触发栈溢出。

修改方案:

  • 移除CustomOAuth2Filter类上的@Component注解,避免重复注册实例
  • 在filterChain方法中直接注入已创建好的CustomOAuth2Filter实例,而非重新创建

修改后的配置类核心代码:

@Bean
public CustomOAuth2Filter customOAuth2Filter(AuthenticationManager authenticationManager) {
    CustomOAuth2Filter filter = new CustomOAuth2Filter(authenticationManager);
    return filter;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, CustomOAuth2Filter customOAuth2Filter) throws Exception {
    http
            .authorizeRequests()
            .anyRequest().authenticated() // 建议改为authenticated,确保请求必须通过认证
            .and()
            .addFilterBefore(customOAuth2Filter, BasicAuthenticationFilter.class)
            .oauth2ResourceServer(oauth2 -> oauth2
                    .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
    return http.build();
}

2. 实现同路径下的双认证逻辑

原过滤器的requiresAuthentication仅判断Bearer令牌,导致请求头认证逻辑无法触发。需调整过滤器逻辑,让它处理所有目标路径请求,优先检查请求头认证,再检查JWT令牌(顺序可按需调换)。

调整自定义过滤器逻辑

@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
    // 对所有匹配/path/**的请求都进行认证检查
    return true;
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 先检查内部请求的认证头
    String username = request.getHeader("user");
    String roleStr = request.getHeader("roles");
    if (username != null && roleStr != null && !roleStr.isBlank()) {
        List<String> roles = Arrays.asList(roleStr.split(","));
        List<GrantedAuthority> grantedAuthorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        MyUser user = new MyUser(username, "", grantedAuthorities);
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, grantedAuthorities);
        auth.setDetails(authenticationDetailsSource.buildDetails(request));
        return auth;
    }

    // 内部请求头不存在,检查JWT令牌
    String authHeader = request.getHeader("Authorization");
    if (authHeader != null && authHeader.startsWith("Bearer ")) {
        String token = authHeader.substring(7);
        BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token);
        authenticationToken.setDetails(authenticationDetailsSource.buildDetails(request));
        return getAuthenticationManager().authenticate(authenticationToken);
    }

    // 两种认证方式都不满足,抛出异常(根据业务需求调整)
    throw new BadCredentialsException("No valid authentication found");
}

补充说明

  • 若需优先校验JWT令牌,只需调换上述代码中两个检查块的顺序
  • 需确保MyUser类正确实现UserDetails接口,让Spring Security能正确识别用户信息
  • 内部请求的user、roles请求头需保证可信,可通过网关或网络层限制内部请求来源,防止外部伪造

内容的提问来源于stack exchange,提问作者user26662895

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 21:42:09