Spring Security单个REST端点如何支持OAuth2 JWT与ApiKey双认证
问题根因
双SecurityConfigurerAdapter方案失效原因
两个独立的WebSecurityConfigurerAdapter会各自生成一套完整的Spring Security过滤链,Order值更小的链路会优先匹配请求。当Order=1的ApiKey配置匹配到/api/special/url路径后,会在这套链路内完成全流程处理,Order=2的OAuth2链路根本不会被触发,两套认证逻辑天然互斥。
单HttpSecurity方案失效原因
自定义的ApiKeyAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,父类默认的失败处理逻辑是直接中断过滤链:一旦ApiKey认证抛出异常(比如请求未携带ApiKey、携带的是JWT凭证),就会调用默认的失败处理逻辑直接返回401,后续排在过滤器链靠后的BearerTokenAuthenticationFilter没有执行机会,因此JWT认证流程永远不会被触发。
同时原方案中自定义的ProviderManager仅注册了ApiKey认证Provider,与全局负责JWT认证的管理器相互隔离,进一步加剧了逻辑不通的问题。
正确实现步骤
1. 改造ApiKey认证过滤器
核心规则:只有当请求中确实携带ApiKey格式的凭证时,才执行ApiKey认证校验;如果请求无ApiKey凭证,直接放行过滤链,交给后续JWT认证流程处理,不抛异常中断链路。
改造后的核心代码:
@Slf4j public class ApiKeyAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public ApiKeyAuthenticationFilter(AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher("/api/special/url/**", "GET")); this.setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { String authHeader = request.getHeader("Authorization"); // 非ApiKey格式的请求直接返回空认证Token,不触发认证逻辑 if (authHeader == null || !authHeader.startsWith("ApiKey ")) { return new ApiKeyAuthenticationToken(); } // 提取ApiKey凭证执行校验 String apiKey = authHeader.substring(7); ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(apiKey); return getAuthenticationManager().authenticate(token); } @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 { String authHeader = request.getHeader("Authorization"); // 仅当请求携带ApiKey凭证但校验失败时,才中断流程返回401 if (authHeader != null && authHeader.startsWith("ApiKey ")) { super.unsuccessfulAuthentication(request, response, failed); return; } // 无ApiKey凭证的请求直接放行,交由后续JWT认证流程处理 chain.doFilter(request, response); } }
2. 调整Spring Security配置
- 移除仅包含ApiKeyProvider的自定义
ProviderManager,将ApiKeyAuthenticationProvider注册到全局认证管理器,保证两套认证逻辑可被同一个AuthenticationManager识别处理。 - 保持
ApiKeyAuthenticationFilter在BearerTokenAuthenticationFilter之前的位置。
改造后的配置代码:
@EnableWebSecurity(debug = true) @Profile("securityOn") public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider; @Autowired private OAuth2ResourceServerProperties resourceServerProperties; // 将自定义ApiKey认证Provider注册到全局认证管理器 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(apiKeyAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/actuator/health").permitAll() .antMatchers("/**").authenticated() .and() // 注入改造后的ApiKey过滤器 .addFilterBefore( new ApiKeyAuthenticationFilter(authenticationManager()), BearerTokenAuthenticationFilter.class ) // 保持原有JWT资源服务器配置 .oauth2ResourceServer() .jwt() .jwkSetUri(this.resourceServerProperties.getJwt().getJwkSetUri()); } }
3. 链路逻辑验证
改造后全链路执行逻辑完全符合预期:
- 携带合法ApiKey访问
/api/special/url:ApiKey过滤器校验通过,完成认证,后续JWT过滤器检测到上下文已有有效认证信息直接跳过,请求放行。 - 携带合法JWT访问
/api/special/url:ApiKey过滤器检测到请求无ApiKey凭证,直接放行,后续BearerTokenAuthenticationFilter完成JWT校验,请求放行。 - 携带非法ApiKey/JWT访问任意路径:对应认证校验失败,返回401。
- 携带合法JWT访问非特殊路径:ApiKey过滤器的路径匹配规则仅覆盖
/api/special/url,请求直接跳过该过滤器,由JWT认证流程处理,与原有行为完全一致。
内容的提问来源于stack exchange,提问作者mmelsen
相关产品推荐
相关产品推荐

