없는 경로로 요청을 보내면 404 에러가 떠야하는 데 401 또는 403 에러가 발생하였다.
API 문서
는 Spring Rest Docs
로 사용하고 있다. 그래서 API 문서 경로가 아닌 경우, 에러가 발생하는 데 이것을 핸들링 하지 않았던 것이 문제였다.
JWT
로 인증하는 것을 구현하여 JWT Filter 및 Handler
를 Spring Security
에 추가하였다. 그리고 허용되지 않는 사용자의 경우 401 또는 403 에러
를 발생시키도록 하였다. 그러나 해당 경로가 없는 경우 404 에러
를 보내줘야 하는데 이것을 하지 않았다.
server:
error:
whitelabel:
enabled: false # Whitelabel 페이지 안 보이게
spring:
web:
resources:
add-mappings: false # 리소스를 매핑하지 않음 (404 에러를 발생시키기 위함)
(생략)
우선 해당 설정을 application.yaml
에 추가한다.
API 문서
를 제외한 모든 응답은 JSON
형식으로 하기 위해 Whitelabel
페이지를 꺼주었다.
@Configuration
public class RestDocMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/docs/**") // 해당 경로만 요청 가능
.addResourceLocations("classpath:/static/docs/"); // 해당 경로에 있는 리소스만 매핑
}
}
application.yaml
에서 add-mappings
를 false
로 설정하였기 때문에, Spring Rest Docs
로 만든 API 문서 경로를 허용해주어야 한다.
이렇게 하면 classpath
에 지정한 경로의 리소스를 볼 수 있다.
@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 에러
를 발생시킬 수 있도록 하는 유틸 클래스다.
@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 에러
를 발생시킬 수 있게 추가한다.
@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 에러
를 발생시킬 수 있게 추가한다.
/* 존재하지 않는 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시간이 걸렸다...
참고에 링크한 블로그의 도움을 엄청 받았다 (감사합니다...!!!)
처음부터 에러 핸들링에 신경쓸걸 그랬다ㅠㅠ
다음 프로젝트부터는 이것을 바탕으로 꼼꼼하게 신경써야겠다!