Spring REST API登录失败:如何返回自定义JSON错误响应?
解决Spring Security登录失败返回自定义JSON的问题
为什么@ControllerAdvice捕获不到异常
Spring Security的AuthenticationFailureListener是在过滤器链阶段执行的,早于Spring MVC的控制器处理流程,而@ControllerAdvice只能捕获DispatcherServlet管辖范围内的控制器异常,因此无法监听到这里抛出的异常。
方案一:自定义AuthenticationFailureHandler(推荐)
直接利用Spring Security官方提供的AuthenticationFailureHandler接口,在认证失败时直接写入自定义JSON响应,这是最规范的实现方式:
- 实现
AuthenticationFailureHandler接口:
@Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { private final ObjectMapper objectMapper; public CustomAuthenticationFailureHandler(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 设置响应状态码,推荐用401(未授权)更符合登录失败的语义 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); // 构造自定义响应体 Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("code", "AUTH_FAILED"); errorResponse.put("message", "用户名或密码错误,或登录次数过多"); errorResponse.put("timestamp", LocalDateTime.now().toString()); // 将JSON写入响应 response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } }
- 在Spring Security配置类中绑定该处理器:
@Configuration @EnableWebSecurity public class SecurityConfig { private final CustomAuthenticationFailureHandler failureHandler; public SecurityConfig(CustomAuthenticationFailureHandler failureHandler) { this.failureHandler = failureHandler; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginProcessingUrl("/login") // 匹配你的登录接口路径 .failureHandler(failureHandler) // 指定自定义失败处理器 ); return http.build(); } }
方案二:保留EventListener,直接写入响应
如果不想替换现有AuthenticationFailureListener,可以在Listener中直接通过HttpServletResponse写入JSON:
- 修改
AuthenticationFailureListener:
@Component public class AuthenticationFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { private final LoginAttemptService loginAttemptService; private final ObjectMapper objectMapper; public AuthenticationFailureListener(LoginAttemptService loginAttemptService, ObjectMapper objectMapper) { this.loginAttemptService = loginAttemptService; this.objectMapper = objectMapper; } @Override public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { String username = event.getAuthentication().getName(); try { loginAttemptService.loginFailed(username); // 获取当前请求的响应对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletResponse response = attributes.getResponse(); if (response != null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("code", "LOGIN_FAILED"); errorResponse.put("message", loginAttemptService.getFailureMessage(username)); // 从你的服务获取自定义消息 response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } } } catch (SecurityServiceException | IOException e) { e.printStackTrace(); } } }
注意:这种方式依赖RequestContextHolder获取请求上下文,Web环境下通常可用,但规范性不如方案一。
方案三:自定义ExceptionTranslationFilter处理异常
如果坚持通过抛异常的方式处理,可以扩展ExceptionTranslationFilter捕获SecurityServiceException并转换为JSON响应:
- 实现自定义过滤器:
public class CustomExceptionTranslationFilter extends ExceptionTranslationFilter { private final ObjectMapper objectMapper; public CustomExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, ObjectMapper objectMapper) { super(authenticationEntryPoint); this.objectMapper = objectMapper; } @Override protected void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof SecurityServiceException) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("code", "SECURITY_ERROR"); errorResponse.put("message", exception.getMessage()); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); return; } // 其他异常交给父类默认逻辑处理 super.handleSpringSecurityException(request, response, chain, exception); } }
- 在Security配置中替换默认过滤器:
@Configuration @EnableWebSecurity public class SecurityConfig { private final ObjectMapper objectMapper; public SecurityConfig(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginProcessingUrl("/login") ); // 替换默认的ExceptionTranslationFilter http.addFilterBefore( new CustomExceptionTranslationFilter(new LoginUrlAuthenticationEntryPoint("/login"), objectMapper), FilterSecurityInterceptor.class ); return http.build(); } }
总结
优先选择方案一,这是Spring Security设计的标准处理方式,代码简洁易维护,无需依赖额外上下文或自定义过滤器。
内容的提问来源于stack exchange,提问作者davidvera
相关产品推荐
相关产品推荐

