Spring Boot Security过滤器链permitAll不生效问题求助
Spring Boot Security 多过滤器链permitAll失效问题排查与解决
问题根源分析
过滤器链顺序与匹配范围冲突
resourceServerFilterChain标记@Order(0),优先级高于@Order(1)的apiServerFilterChain,且两个链未设置请求匹配规则,所有请求会先进入资源服务器链处理。同时资源服务器链的authorizeHttpRequests存在语法错误:.and().oauth2ResourceServer()被错误嵌套在lambda中,导致配置未生效,公开端点的permitAll规则被覆盖。过滤器未区分公开端点
ApiFilter和ApiKeyFilter会对进入apiServerFilterChain的所有请求执行验证逻辑,包括已配置permitAll的公开端点,直接抛出BadCredentialsException导致访问失败。多链未明确划分请求范围
两个过滤器链均处理所有路径,未通过requestMatcher明确划分UI与API请求的处理边界,导致请求路由混乱,permitAll规则无法精准生效。
分步解决方案
1. 修正过滤器链的请求匹配与顺序
给每个链添加明确的请求匹配规则,避免交叉处理:
// 资源服务器链(Keycloak UI端):仅处理带X-GUI头的请求 @Bean @Order(0) public SecurityFilterChain resourceServerFilterChain( HttpSecurity http, @Qualifier("corsConfigurationSource") CorsConfigurationSource corsConfigurationSource ) throws Exception { List<String> allRoles = subscriptions.stream() .map(sub -> sub.getRoles().split(",")) .flatMap(Arrays::stream) .collect(Collectors.toList()); allRoles.add("ORGANIZATION_ADMIN"); allRoles.add("ORGANIZATION_USER"); http .requestMatcher(request -> "true".equals(request.getHeader("X-GUI"))) .authorizeHttpRequests(authorize -> authorize .requestMatchers(GenericAbstractControllerInterface.API_PUBLIC_URI + "/**").permitAll() .anyRequest().hasAnyRole(allRoles.toArray(new String[0])) ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .cors(cors -> cors.configurationSource(corsConfigurationSource)) .csrf(csrf -> csrf.disable()); return http.build(); } // API端过滤器链:仅处理API请求 @Bean @Order(1) @DependsOn("corsConfigurationSource") public SecurityFilterChain apiServerFilterChain( HttpSecurity http, @Qualifier("corsConfigurationSource") CorsConfigurationSource corsConfigurationSource ) throws Exception { http .requestMatcher(ApiContext::isApi) .authorizeHttpRequests(authorize -> authorize .requestMatchers( GenericAbstractControllerInterface.PUBLIC_API_DOC_BASE_URI + "/**", GenericAbstractControllerInterface.API_PUBLIC_URI + "/**" ).permitAll() .anyRequest().authenticated() ) .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(withDefaults()) .cors(cors -> cors.configurationSource(corsConfigurationSource)) .csrf(AbstractHttpConfigurer::disable); return http.build(); }
2. 修改ApiKeyFilter,跳过公开端点验证
在过滤器中判断请求是否为公开端点,若是直接放行:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestUri = request.getRequestURI(); logger.info("ApiKeyFilter invoked for request: " + requestUri); // 公开端点直接跳过验证 if (requestUri.startsWith(GenericAbstractControllerInterface.API_PUBLIC_URI) || requestUri.startsWith(GenericAbstractControllerInterface.PUBLIC_API_DOC_BASE_URI)) { filterChain.doFilter(request, response); return; } if (ApiContext.isApi()) { // 原有API Key验证逻辑保留 String requestApiKey = request.getHeader("X-API-KEY"); String requestApiSecret = request.getHeader("X-SECRET-KEY"); if (requestApiKey == null || requestApiSecret == null) { throw new BadCredentialsException("BadCredentials"); } Optional<ApiInformation> apiInformationOptional = this.apiInformationRepository.findByApiKey(requestApiKey); if (!apiInformationOptional.isPresent()) { throw new BadCredentialsException("BadCredentials"); } ApiInformation apiInformation = apiInformationOptional.get(); if (!apiInformation.getApiKey().equals(requestApiKey) || !apiInformation.getSecretKey().equals(requestApiSecret)) { throw new BadCredentialsException("BadCredentials"); } Optional<TenantInformation> tenantInformationOptional = this.tenantInformationRepository.findByOrganization(apiInformation.getOrganization()); if (!tenantInformationOptional.isPresent()) { throw new BadCredentialsException("BadCredentials"); } TenantInformation tenantInformation = tenantInformationOptional.get(); Authentication authentication = new UsernamePasswordAuthenticationToken(apiInformation.getApiKey(), null, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authentication); TenantContext.setCurrentTenant(tenantInformation.getTenantId()); } else if (ApiContext.isActuator()) { Authentication authentication = new UsernamePasswordAuthenticationToken("GenericUser", null, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); }
3. 修正资源服务器链的配置语法
将oauth2ResourceServer配置从authorizeHttpRequests的lambda中移出,确保配置生效:
// 错误写法(嵌套在authorize内) authorizeRequests.requestMatchers(...).permitAll().anyRequest().authenticated().and().oauth2ResourceServer(...) // 正确写法 authorizeRequests .requestMatchers(...).permitAll() .anyRequest().authenticated(); // 回到HttpSecurity上下文配置oauth2 http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)));
API Key验证实现优化建议
当前过滤器实现不符合Spring Security标准流程,建议改用AuthenticationProvider+自定义Token的方式,更便于扩展维护:
- 自定义API Key认证Token:
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { private final String apiKey; private final String secretKey; public ApiKeyAuthenticationToken(String apiKey, String secretKey) { super(null); this.apiKey = apiKey; this.secretKey = secretKey; setAuthenticated(false); } public ApiKeyAuthenticationToken(String apiKey, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.apiKey = apiKey; this.secretKey = null; setAuthenticated(true); } @Override public Object getCredentials() { return secretKey; } @Override public Object getPrincipal() { return apiKey; } }
- 自定义AuthenticationProvider:
@Component public class ApiKeyAuthenticationProvider implements AuthenticationProvider { private final ApiInformationRepository apiInformationRepository; private final TenantInformationRepository tenantInformationRepository; public ApiKeyAuthenticationProvider(ApiInformationRepository apiInformationRepository, TenantInformationRepository tenantInformationRepository) { this.apiInformationRepository = apiInformationRepository; this.tenantInformationRepository = tenantInformationRepository; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { ApiKeyAuthenticationToken token = (ApiKeyAuthenticationToken) authentication; String apiKey = token.getPrincipal().toString(); String secretKey = token.getCredentials().toString(); ApiInformation apiInfo = apiInformationRepository.findByApiKey(apiKey) .orElseThrow(() -> new BadCredentialsException("BadCredentials")); if (!apiInfo.getSecretKey().equals(secretKey)) { throw new BadCredentialsException("BadCredentials"); } TenantInformation tenantInfo = tenantInformationRepository.findByOrganization(apiInfo.getOrganization()) .orElseThrow(() -> new BadCredentialsException("BadCredentials")); TenantContext.setCurrentTenant(tenantInfo.getTenantId()); return new ApiKeyAuthenticationToken(apiKey, Collections.emptyList()); } @Override public boolean supports(Class<?> authentication) { return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication); } }
- 自定义过滤器触发认证:
@Component public class ApiKeyAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public ApiKeyAuthenticationFilter() { super(new AntPathRequestMatcher("/api/**", "GET,POST,PUT,DELETE")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String apiKey = request.getHeader("X-API-KEY"); String secretKey = request.getHeader("X-SECRET-KEY"); if (apiKey == null || secretKey == null) { throw new BadCredentialsException("BadCredentials"); } ApiKeyAuthenticationToken authRequest = new ApiKeyAuthenticationToken(apiKey, secretKey); return getAuthenticationManager().authenticate(authRequest); } }
- 在API过滤器链中配置:
// 替换原有ApiKeyFilter .addFilterBefore(apiKeyAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 注册自定义Provider .authenticationProvider(apiKeyAuthenticationProvider)
内容的提问来源于stack exchange,提问作者the_student
相关产品推荐
相关产品推荐

