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,原因主要有两个:
SecurityConfig中手动new ApiKeyAuthFilter(),而非注入Spring容器管理的实例,导致过滤器异常无法和全局异常处理联动。- 过滤器抛出异常后,未正确传递给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

