同一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
相关产品推荐
相关产品推荐

