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

Spring Boot 3+Spring Security 6自定义无效API密钥错误消息失效解决

问题:Spring Boot 3 + Spring Security 6 自定义无效API密钥错误消息

我正在开发基于Spring Boot 3和Spring Security 6的应用,需要自定义无效API密钥的错误消息:当提供无效API密钥时,希望返回"Invalid API Key",而非默认的"Full authentication is required to access this resource."。

以下是我已实现的类:

CustomAuthenticationEntryPoint

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json");

        Map<String, Object> data = new HashMap<>();
        data.put("message", authException.getMessage());
        data.put("error", HttpStatus.UNAUTHORIZED);

        response.getOutputStream().println(objectMapper.writeValueAsString(data));
    }
}

ApiKeyAuthFilter

@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = Logger.getLogger(ApiKeyAuthFilter.class.getName());
    private static final String API_KEY_HEADER = "X-API-KEY";
    private static final String VALID_API_KEY = "valid-api-key";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ApiKeyAuthenticationException {
        String apiKey = request.getHeader(API_KEY_HEADER);
        LOGGER.info("API Key received: " + apiKey);

        if (VALID_API_KEY.equals(apiKey)) {
            UserDetails userDetails = User.withUsername("apiKeyUser")
                    .password("")
                    .authorities(Collections.emptyList())
                    .build();

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
            LOGGER.info("Authentication set in security context");
        } else {
            throw new ApiKeyAuthenticationException("Invalid API Key");
        }
        filterChain.doFilter(request, response);
    }
}

ApiKeyAuthenticationException

public class ApiKeyAuthenticationException extends AuthenticationException {
    public ApiKeyAuthenticationException(String message) {
        super(message);
    }
}

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    public SecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint) {
        this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated())
                .addFilterBefore(new ApiKeyAuthFilter(), BasicAuthenticationFilter.class)
                .exceptionHandling(exceptionHandling ->
                        exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPoint));

        return http.build();
    }
}

尽管完成了上述实现,返回的错误消息仍然是默认内容。请问如何确保提供无效API密钥时返回自定义的"Invalid API Key"错误消息?

更新:
我发现在ApiKeyAuthFilter类中直接捕获AuthenticationException可以修改响应文本,但我认为这不是最优方案。以下是更新后的ApiKeyAuthFilter类:

@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = Logger.getLogger(ApiKeyAuthFilter.class.getName());
    private static final String API_KEY_HEADER = "X-API-KEY";
    private static final String VALID_API_KEY = "valid-api-key";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ApiKeyAuthenticationException {
        String apiKey = request.getHeader(API_KEY_HEADER);
        LOGGER.info("API Key received: " + apiKey);

        try {

            if (VALID_API_KEY.equals(apiKey)) {
                UserDetails userDetails = User.withUsername("apiKeyUser")
                        .password("")
                        .authorities(Collections.emptyList())
                        .build();

                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
                LOGGER.info("Authentication set in security context");
                filterChain.doFilter(request, response);
            } else {
                throw new ApiKeyAuthenticationException("Invalid API Key");
            }

        } catch (AuthenticationException e) {
            SecurityContextHolder.clearContext();
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"message\": \"" + e.getMessage() + "\", \"error\": \"UNAUTHORIZED\"}");
        }

    }
}

解决方案

你的核心问题在于自定义的AuthenticationEntryPoint没有接收到你抛出的ApiKeyAuthenticationException,原因主要有两个:

  1. SecurityConfig中手动new ApiKeyAuthFilter(),而非注入Spring容器管理的实例,导致过滤器异常无法和全局异常处理联动。
  2. 过滤器抛出异常后,未正确传递给Spring Security的异常处理机制,被默认逻辑拦截返回了默认消息。

正确实现步骤:

1. 修复SecurityConfig的过滤器注入

不要手动创建过滤器实例,通过构造函数注入Spring容器管理的ApiKeyAuthFilter:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final ApiKeyAuthFilter apiKeyAuthFilter;

    public SecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint, ApiKeyAuthFilter apiKeyAuthFilter) {
        this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
        this.apiKeyAuthFilter = apiKeyAuthFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated())
                .addFilterBefore(apiKeyAuthFilter, BasicAuthenticationFilter.class)
                .exceptionHandling(exceptionHandling ->
                        exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPoint));

        return http.build();
    }
}

2. 还原ApiKeyAuthFilter,移除内部异常捕获

过滤器只负责认证判断,异常交给全局AuthenticationEntryPoint处理,不要在内部捕获:

@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = Logger.getLogger(ApiKeyAuthFilter.class.getName());
    private static final String API_KEY_HEADER = "X-API-KEY";
    private static final String VALID_API_KEY = "valid-api-key";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String apiKey = request.getHeader(API_KEY_HEADER);
        LOGGER.info("API Key received: " + apiKey);

        if (VALID_API_KEY.equals(apiKey)) {
            UserDetails userDetails = User.withUsername("apiKeyUser")
                    .password("")
                    .authorities(Collections.emptyList())
                    .build();

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
            LOGGER.info("Authentication set in security context");
            filterChain.doFilter(request, response);
        } else {
            throw new ApiKeyAuthenticationException("Invalid API Key");
        }
    }
}

注意:移除方法声明中的ApiKeyAuthenticationException,因为OncePerRequestFilter的doFilterInternal仅允许抛出ServletException和IOException,自定义异常作为AuthenticationException子类会被自动包装为ServletException抛出,不影响全局处理。

3. 保留CustomAuthenticationEntryPoint的原有实现

你的CustomAuthenticationEntryPoint逻辑正确,会捕获AuthenticationException并返回包含自定义消息的JSON响应。

为什么这是最优方案:

  • 遵循Spring Security设计原则,认证逻辑与异常处理解耦,符合单一职责原则。
  • 便于后续扩展,如需针对不同异常返回不同消息,只需修改CustomAuthenticationEntryPoint,无需改动过滤器。
  • 保持代码整洁可维护,统一管理全局异常响应格式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 11:50:56