spring boot中自定义安全认证异常需区分位置:过滤器链中的authenticationexception须用authenticationentrypoint处理,controller层异常才可用@exceptionhandler;oauth2等场景需重写对应entrypoint。

认证异常发生在过滤器链中,@ControllerAdvice 捕不到
比如用户名密码错误、Token 无效、客户端凭证不合法等,这些都由 Spring Security 的过滤器(如 UsernamePasswordAuthenticationFilter、OAuth2AuthenticationProcessingFilter)提前拦截并抛出 AuthenticationException 及其子类(如 BadCredentialsException、InvalidTokenException)。此时请求根本没进到 Controller,所以 @RestControllerAdvice 里的 @ExceptionHandler(AuthenticationException.class) 完全不会触发。
正确做法是实现 Spring Security 提供的专用入口处理器:
- AuthenticationEntryPoint:处理“未认证”场景,例如匿名用户访问 /api/user,或认证过程抛出 AuthenticationException
- AccessDeniedHandler:处理“已认证但无权限”场景,抛出 AccessDeniedException
以自定义认证失败响应为例(返回 JSON 而非跳转登录页):
@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(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
String message = authException.getMessage();
// 可根据具体异常类型细化响应内容
if (authException instanceof BadCredentialsException) {
message = "用户名或密码错误";
} else if (authException instanceof DisabledException) {
message = "账户已被禁用";
}
response.getWriter().write(objectMapper.writeValueAsString(
Map.of("code", "AUTH_FAILED", "message", message, "timestamp", System.currentTimeMillis())
));
}
}
再在 Security 配置中注册它:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(e -> e
.authenticationEntryPoint(customAuthenticationEntryPoint()) // ← 注入
.accessDeniedHandler(customAccessDeniedHandler())
);
return http.build();
}
自定义 AuthenticationException 子类需单独处理
如果你自己定义了 CustomAuthenticationException extends AuthenticationException,并在认证逻辑中主动 throw,注意:@ExceptionHandler 只匹配精确类型,不自动继承父类处理器。
也就是说,即使你写了 @ExceptionHandler(AuthenticationException.class),它也不会捕获 CustomAuthenticationException——除非你显式声明:
@RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(CustomAuthenticationException.class)
public ResponseEntity<errorresponse> handleCustomAuthException(CustomAuthenticationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("CUSTOM_AUTH_FAIL", e.getMessage()));
}
}</errorresponse>
⚠️ 但这个 handler 只在异常“到达 Controller 层后”才生效。如果该异常是在 AuthenticationProvider.authenticate() 中抛出且未被过滤器链拦截(极少见),或你在 Controller 里手动 throw,它才起作用。
登录失败时用 AuthenticationFailureHandler 更直接
对于表单登录(formLogin())场景,推荐使用 AuthenticationFailureHandler 接口,它专为登录失败设计,天然支持获取原始请求、响应和异常信息:
@Component
public class JsonLoginFailureHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
String msg = "登录失败";
if (exception instanceof BadCredentialsException) {
msg = "账号或密码不正确";
} else if (exception instanceof AccountExpiredException) {
msg = "账户已过期";
}
response.getWriter().write(objectMapper.writeValueAsString(
Map.of("success", false, "message", msg)
));
}
}
然后配置进 Security:
http.formLogin(form -> form
.failureHandler(jsonLoginFailureHandler()) // ← 替代 failureUrl()
);
OAuth2 场景要重写 OAuth2AuthenticationEntryPoint
如果是 OAuth2 授权码/密码模式,异常(如 invalid_client、invalid_token)由 OAuth2AuthenticationEntryPoint 处理。你需要继承它并重写 commence() 方法,才能统一输出 JSON 格式错误,避免与业务异常响应格式割裂。
常见错误是试图用 @ExceptionHandler 拦截 InvalidTokenException ——它根本不会路过 Controller。
不复杂但容易忽略











