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

Spring Boot Security过滤器链permitAll不生效问题求助

Spring Boot Security 多过滤器链permitAll失效问题排查与解决

问题根源分析

  1. 过滤器链顺序与匹配范围冲突
    resourceServerFilterChain标记@Order(0),优先级高于@Order(1)的apiServerFilterChain,且两个链未设置请求匹配规则,所有请求会先进入资源服务器链处理。同时资源服务器链的authorizeHttpRequests存在语法错误:.and().oauth2ResourceServer()被错误嵌套在lambda中,导致配置未生效,公开端点的permitAll规则被覆盖。

  2. 过滤器未区分公开端点
    ApiFilter和ApiKeyFilter会对进入apiServerFilterChain的所有请求执行验证逻辑,包括已配置permitAll的公开端点,直接抛出BadCredentialsException导致访问失败。

  3. 多链未明确划分请求范围
    两个过滤器链均处理所有路径,未通过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的方式,更便于扩展维护:

  1. 自定义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;
    }
}
  1. 自定义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);
    }
}
  1. 自定义过滤器触发认证:
@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);
    }
}
  1. 在API过滤器链中配置:
// 替换原有ApiKeyFilter
.addFilterBefore(apiKeyAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 注册自定义Provider
.authenticationProvider(apiKeyAuthenticationProvider)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 04:45:56