Java RestApi中ResponseStatusException的401/403无响应问题排查
解决Spring Boot中401/403状态无法返回结构化错误响应的问题
问题根源
Spring Boot中,401(UNAUTHORIZED)和403(FORBIDDEN)状态会被Spring Security的默认过滤器优先拦截处理,比如ExceptionTranslationFilter、BasicAuthenticationFilter,这些过滤器会直接向响应流写入空白或极简响应,完全绕过了Spring MVC的全局异常处理器和默认错误处理机制——这就是为什么其他状态码(400、404等)能正常返回结构化响应,唯独这俩不行。
解决方案
根据是否使用Spring Security,分两种场景处理:
场景1:项目使用Spring Security(最常见情况)
自定义AuthenticationEntryPoint(处理401)和AccessDeniedHandler(处理403),让它们返回统一的结构化响应。
- 先定义通用错误响应类:
import java.time.LocalDateTime; public class ErrorResponse { private LocalDateTime timestamp; private int status; private String error; private String message; private String path; // 构造器、getter、setter自行补充 public ErrorResponse(LocalDateTime timestamp, int status, String error, String message, String path) { this.timestamp = timestamp; this.status = status; this.error = error; this.message = message; this.path = path; } }
- 自定义401处理器:
import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; import java.time.LocalDateTime; @Component public class CustomAuthEntryPoint implements AuthenticationEntryPoint { private final ObjectMapper objectMapper; public CustomAuthEntryPoint(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); ErrorResponse responseBody = new ErrorResponse( LocalDateTime.now(), HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized", authException.getMessage(), request.getRequestURI() ); objectMapper.writeValue(response.getOutputStream(), responseBody); } }
- 自定义403处理器:
import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.time.LocalDateTime; @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { private final ObjectMapper objectMapper; public CustomAccessDeniedHandler(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_FORBIDDEN); ErrorResponse responseBody = new ErrorResponse( LocalDateTime.now(), HttpServletResponse.SC_FORBIDDEN, "Forbidden", accessDeniedException.getMessage(), request.getRequestURI() ); objectMapper.writeValue(response.getOutputStream(), responseBody); } }
- 在Security配置中注入自定义处理器:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { private final CustomAuthEntryPoint authEntryPoint; private final CustomAccessDeniedHandler accessDeniedHandler; public SecurityConfig(CustomAuthEntryPoint authEntryPoint, CustomAccessDeniedHandler accessDeniedHandler) { this.authEntryPoint = authEntryPoint; this.accessDeniedHandler = accessDeniedHandler; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 这里替换成你的权限规则 .anyRequest().authenticated() ) .exceptionHandling(ex -> ex .authenticationEntryPoint(authEntryPoint) .accessDeniedHandler(accessDeniedHandler) ); return http.build(); } }
场景2:项目未使用Spring Security
通过配置让Spring MVC完全接管错误处理,禁用容器级别的拦截:
- 在
application.properties中添加:
server.error.whitelabel.enabled=false spring.mvc.throw-exception-if-no-handler-found=true spring.web.resources.add-mappings=false
- 编写全局异常处理器捕获
ResponseStatusException:
import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResponseStatusException.class) public ResponseEntity<ErrorResponse> handleResponseStatusException(ResponseStatusException ex, HttpServletRequest request) { ErrorResponse responseBody = new ErrorResponse( LocalDateTime.now(), ex.getStatusCode().value(), ex.getStatusCode().getReasonPhrase(), ex.getReason(), request.getRequestURI() ); return new ResponseEntity<>(responseBody, ex.getStatusCode()); } }
注意事项
- 如果是在控制器中手动抛出
ResponseStatusException(401/403),需确保请求未被Spring Security的前置过滤器拦截(比如已通过认证校验),否则自定义处理器依然不会生效。 - 检查项目中是否存在其他自定义过滤器,避免它们提前写入401/403响应。
内容的提问来源于stack exchange,提问作者Ojam
相关产品推荐
相关产品推荐

