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

Spring REST API登录失败:如何返回自定义JSON错误响应?

解决Spring Security登录失败返回自定义JSON的问题

为什么@ControllerAdvice捕获不到异常

Spring Security的AuthenticationFailureListener是在过滤器链阶段执行的,早于Spring MVC的控制器处理流程,而@ControllerAdvice只能捕获DispatcherServlet管辖范围内的控制器异常,因此无法监听到这里抛出的异常。

方案一:自定义AuthenticationFailureHandler(推荐)

直接利用Spring Security官方提供的AuthenticationFailureHandler接口,在认证失败时直接写入自定义JSON响应,这是最规范的实现方式:

  1. 实现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));
    }
}
  1. 在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:

  1. 修改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响应:

  1. 实现自定义过滤器:
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);
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 14:40:33