[Spring] 존재하지 않는 API 요청 시, 401 에러를 404로 수정

General Dong·2024년 9월 30일
0

문제 발생

없는 경로로 요청을 보내면 404 에러가 떠야하는 데 401 또는 403 에러가 발생하였다.

문제 원인

  1. API 문서Spring Rest Docs로 사용하고 있다. 그래서 API 문서 경로가 아닌 경우, 에러가 발생하는 데 이것을 핸들링 하지 않았던 것이 문제였다.

  2. JWT로 인증하는 것을 구현하여 JWT Filter 및 HandlerSpring Security에 추가하였다. 그리고 허용되지 않는 사용자의 경우 401 또는 403 에러를 발생시키도록 하였다. 그러나 해당 경로가 없는 경우 404 에러를 보내줘야 하는데 이것을 하지 않았다.

문제 해결

application.yaml

server:
  error:
    whitelabel:
      enabled: false  # Whitelabel 페이지 안 보이게

spring:
  web:
    resources:
      add-mappings: false # 리소스를 매핑하지 않음 (404 에러를 발생시키기 위함)

(생략)

우선 해당 설정을 application.yaml에 추가한다.
API 문서를 제외한 모든 응답은 JSON 형식으로 하기 위해 Whitelabel 페이지를 꺼주었다.

WebMvcConfig 설정

@Configuration
public class RestDocMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/docs/**") // 해당 경로만 요청 가능
                .addResourceLocations("classpath:/static/docs/");   // 해당 경로에 있는 리소스만 매핑
    }
}

application.yaml에서 add-mappingsfalse로 설정하였기 때문에, Spring Rest Docs로 만든 API 문서 경로를 허용해주어야 한다.
이렇게 하면 classpath에 지정한 경로의 리소스를 볼 수 있다.

JWT Handler에서 404 에러 응답 추가

Endpoint 확인 및 에러 응답 메시지 생성

@Component
@RequiredArgsConstructor
public class HttpRequestEndpointUtil {

    private final DispatcherServlet servlet;

    /* Handling 여부로 Endpoint 존재 판단 */
    boolean isEndpointExist(HttpServletRequest request) {
        for (HandlerMapping handlerMapping : servlet.getHandlerMappings()) {
            try {
                HandlerExecutionChain foundHandler = handlerMapping.getHandler(request);
                if (foundHandler != null) {
                    return true;
                }
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }

    /* 에러에 따른 Response Body 만들기 */
    ErrorDto makeErrorBody(ErrorCode errorCode, HttpServletRequest request) {
        Optional<ErrorDto> errorBody = Optional.ofNullable(ErrorDto.builder()
                .status(errorCode.getStatus())
                .error(errorCode.getError())
                .message(errorCode.getMessage() + " url: " + request.getRequestURI())
                .build());

        return errorBody.orElseThrow(() -> new CustomException(ErrorCode.SERVER_ERROR));
    }
}

HttpRequestEndpointUtil은 JWT Handler가 404 에러를 발생시킬 수 있도록 하는 유틸 클래스다.

JwtAuthenticationEntryPoint

@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint extends Http403ForbiddenEntryPoint {

    private final HttpRequestEndpointUtil requestEndpointUtil;

    /* 유효한 자격증명을 제공하지 않고 접근하려 할때 401 Unauthorized Error 보냄 */
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        ErrorDto errorBody;

        if (requestEndpointUtil.isEndpointExist(request)) {
            errorBody = requestEndpointUtil.makeErrorBody(ErrorCode.UNAUTHORIZED, request);

            response.setContentType(MediaType.APPLICATION_JSON_VALUE);  // 응답 메시지 타입 JSON으로 설정
            response.setStatus(errorBody.getStatus());    // 에러 응답 메시지 Status 설정

            // JSON으로 변환하여 전송
            ObjectMapper json = new ObjectMapper();
            json.writeValue(response.getOutputStream(), errorBody);
        } else {
            errorBody = requestEndpointUtil.makeErrorBody(ErrorCode.NOT_FOUND_PATH, request);

            response.setContentType(MediaType.APPLICATION_JSON_VALUE);  // 응답 메시지 타입 JSON으로 설정
            response.setStatus(errorBody.getStatus());    // 에러 응답 메시지 Status 설정

            // JSON으로 변환하여 전송
            ObjectMapper json = new ObjectMapper();
            json.writeValue(response.getOutputStream(), errorBody);
        }
    }
}

Spring Security에 등록한 401 에러를 핸들러 하기 위한 컴포넌트에 Endpoint 여부를 확인하여 404 에러를 발생시킬 수 있게 추가한다.

JwtAcessDeniedHandler

@Component
@RequiredArgsConstructor
public class JwtAcessDeniedHandler extends AccessDeniedHandlerImpl {

    private final HttpRequestEndpointUtil requestEndpointUtil;

    /* 필요한 권한이 없이 접근하려 할때 403 Forbidden Error 보냄 */
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {

        ErrorDto errorBody;

        if (requestEndpointUtil.isEndpointExist(request)) {
            errorBody = requestEndpointUtil.makeErrorBody(ErrorCode.FORBIDDEN, request);

            response.setContentType(MediaType.APPLICATION_JSON_VALUE);  // 응답 메시지 타입 JSON으로 설정
            response.setStatus(errorBody.getStatus());    // 에러 응답 메시지 Status 설정

            // JSON으로 변환하여 전송
            ObjectMapper json = new ObjectMapper();
            json.writeValue(response.getOutputStream(), errorBody);
        } else {
            errorBody = requestEndpointUtil.makeErrorBody(ErrorCode.NOT_FOUND_PATH, request);

            response.setContentType(MediaType.APPLICATION_JSON_VALUE);  // 응답 메시지 타입 JSON으로 설정
            response.setStatus(errorBody.getStatus());    // 에러 응답 메시지 Status 설정

            // JSON으로 변환하여 전송
            ObjectMapper json = new ObjectMapper();
            json.writeValue(response.getOutputStream(), errorBody);
        }
    }
}

Spring Security에 등록한 403 에러를 핸들러 하기 위한 컴포넌트에 Endpoint 여부를 확인하여 404 에러를 발생시킬 수 있게 추가한다.

404 에러 커스텀 응답

/* 존재하지 않는 URL로 요청 시 */
@ExceptionHandler({NoHandlerFoundException.class, NoResourceFoundException.class})
public ResponseEntity < ErrorDto > handleUnknownResource(HttpServletRequest request) {
  return toCustomErrorDto(ErrorCode.NOT_FOUND_PATH, request);
}

/* Custom Error Response 생성 */
private ResponseEntity < ErrorDto > toCustomErrorDto(ErrorCode errorCode, HttpServletRequest request) {
  log.error("Global Exception, status: {}, error: {}, message: {}, requested url: {}, timestemp: {}",
    errorCode.getStatus(), errorCode.getError(), errorCode.getMessage(),
    request.getMethod().concat(" ").concat(request.getServletPath()), LocalDateTime.now());

  return ResponseEntity.status(errorCode.getStatus())
    .body(ErrorDto.builder()
      .path(request.getServletPath())
      .status(errorCode.getStatus())
      .error(errorCode.getError())
      .message(errorCode.getMessage())
      .build());
}

WebMvcConfig에서 설정한 classpath 외 리소스를 요청 시, NoResourceFoundException 에러가 발생한다.
또한 API로 존재하지 않는 URL로 요청 시, NoHandlerFoundException 에러가 발생하게 된다.
해당 에러들을 핸들링 하기 위해 @RestControllerAdvice로 지정한 클래스에 위 코드를 추가하여 404 에러를 응답 하였다.

소감

이거 해결하느라 4시간이 걸렸다...
참고에 링크한 블로그의 도움을 엄청 받았다 (감사합니다...!!!)

처음부터 에러 핸들링에 신경쓸걸 그랬다ㅠㅠ
다음 프로젝트부터는 이것을 바탕으로 꼼꼼하게 신경써야겠다!


참고

민둘님 블로그
ydh6226님 블로그

profile
개발에 대한 기록과 복습을 위한 블로그 | Back-end Developer

0개의 댓글