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

Spring Boot OAuth2 Resource Server自定义404错误异常处理问题

问题:Spring Boot + OAuth2 Resource Server下自定义404响应被AuthenticationEntryPoint拦截

问题现象

访问不存在的API时,始终返回自定义的401未授权响应,而非预期的404格式响应。已配置RestControllerAdvice处理NoHandlerFoundException,也添加了相关配置参数,但异常始终被CustomAuthenticationEntryPoint捕获。

现有配置代码

SecurityFilterChain配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -> csrf.disable())
            .cors().and()
            .authorizeHttpRequests(auths -> auths
                    .antMatchers(ALLOW_LIST_URIS).permitAll()
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                    .jwt(jwt -> jwt
                            .jwtAuthenticationConverter(jwtAuthenticationConverter())))
            .oauth2ResourceServer(oAuth2 -> oAuth2.authenticationEntryPoint(authenticationEntryPoint))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();
}

CustomAuthenticationEntryPoint实现

@Slf4j
@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final Jackson2ObjectMapperBuilder mapperBuilder;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException {
        log.warn("Someone has tried to access without Authentication(AuthenticationException): {}",
                authException.getMessage());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        mapperBuilder.build()
                .writeValue(response.getOutputStream(), new ErrorResDto(HttpStatus.UNAUTHORIZED, UNAUTHORIZED_MESSAGE));
        response.getOutputStream().flush();
    }
}

已尝试的配置参数

spring.mvc.throw-exception-if-no-handler-found=true
server.error.whitelabel.enabled=false
spring.web.resources.add-mappings=false

自定义异常处理器(RestControllerAdvice)

@Slf4j
@RestControllerAdvice
public class CustomExceptionHandling {
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<ErrorResDto> handleNoHandlerFoundException (NoHandlerFoundException e) {
        log.error("NoHandlerFoundException init");
        return createHttpResponse(HttpStatus.NOT_FOUND, MAPPING_NOT_FOUND);
    }

    private ResponseEntity<ErrorResDto> createHttpResponse(HttpStatus httpStatus, String message) {
        ErrorResDto errorResponse = new ErrorResDto(httpStatus, message);
        return new ResponseEntity<>(errorResponse, errorResponse.getHttpStatus());
    }
}

当前与期望响应

当前错误响应(401)

{
    "rc": 401,
    "message": "Full authentication is required to access this resource",
    "httpStatus": "UNAUTHORIZED"
}

期望的404响应

{
    "rc": 404,
    "message": "Path not found",
    "httpStatus": "NOT_FOUND"
}

解决方法

原因分析

Spring Security过滤器链执行优先级高于Spring MVC的DispatcherServlet。访问不存在的API时,Security先拦截请求并检查认证状态,若未携带有效凭证,直接触发AuthenticationEntryPoint返回401,DispatcherServlet没有机会处理请求并抛出NoHandlerFoundException。

具体解决方案

方案1:在AuthenticationEntryPoint中提前判断请求路径是否存在

注入RequestMappingHandlerMapping,在处理认证异常前先检查请求是否有对应的处理器,无匹配则直接返回404:

@Slf4j
@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final Jackson2ObjectMapperBuilder mapperBuilder;
    private final RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException {
        try {
            // 查找当前请求对应的处理器
            HandlerExecutionChain handlerExecutionChain = requestMappingHandlerMapping.getHandler(request);
            if (handlerExecutionChain == null) {
                // 无匹配处理器,返回404
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                mapperBuilder.build()
                        .writeValue(response.getOutputStream(), new ErrorResDto(HttpStatus.NOT_FOUND, "Path not found"));
                response.getOutputStream().flush();
                return;
            }
        } catch (Exception e) {
            log.error("Failed to check request handler", e);
        }

        // 有匹配处理器,返回401
        log.warn("Someone has tried to access without Authentication(AuthenticationException): {}",
                authException.getMessage());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        mapperBuilder.build()
                .writeValue(response.getOutputStream(), new ErrorResDto(HttpStatus.UNAUTHORIZED, UNAUTHORIZED_MESSAGE));
        response.getOutputStream().flush();
    }
}

方案2:使用ErrorController统一处理所有错误

实现ErrorController,统一管理404、401等所有错误响应,避免Security与MVC的异常处理冲突:

@RestController
@RequestMapping("/error")
public class CustomErrorController implements ErrorController {
    private final ErrorAttributes errorAttributes;

    public CustomErrorController(ErrorAttributes errorAttributes) {
        this.errorAttributes = errorAttributes;
    }

    @GetMapping
    public ResponseEntity<ErrorResDto> handleError(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        String message = switch (status.value()) {
            case 404 -> "Path not found";
            case 401 -> "Full authentication is required to access this resource";
            default -> status.getReasonPhrase();
        };
        return ResponseEntity.status(status).body(new ErrorResDto(status, message));
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        try {
            return HttpStatus.valueOf(statusCode);
        } catch (Exception ex) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }
}

内容的提问来源于stack exchange,提问作者helmigandi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 15:05:30