public class JwtInterceptor extends HandlerInterceptorAdapter {
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(JwtInterceptor.class);
@Autowired
private TokenService tokenService;
private static final String SWAGGER_URI = "swagger-ui.html";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws AuthenticationException {
if (request.getMethod().equals("OPTIONS")) {
return true;
}
String jwt = request.getHeader("Authorization");
if (jwt == null) {
LOG.error("[JwtInterceptor] JWT is null");
throw new AuthenticationException("JWT is required");
}
try {
ResultJWT resultJWT = tokenService.getResultJWT(jwt);
if (request.getRequestURI().contains(SWAGGER_URI)
&& ((resultJWT.getRole() == null) || JWTRoles.USER.getRole().equals(resultJWT.getRole()))) {
LOG.error("[JwtInterceptor] JWT Role is not Allowed");
throw new AuthenticationException("JWT Role is not Allowed");
}
} catch (JwtException e) {
LOG.error("[JwtInterceptor] JwtException Throw");
throw e;
}
return true;
}
}
현재 개발하고 있는 프로젝트단에서 JWT를 활용한 인증이 필요한 Handler를
공통적으로 인증 과정을 수행하는 HandlerInterceptorAdapter
를 상속받는 JwtInterceptor
를 위와 같이 사용하고 있었다.
위 인증 과정 중 인증 실패시 아래 처럼 Exception을 throw 하는 부분이 있는데,
throw new AuthenticationException("JWT is required");
해당 프로젝트에서 @ControllerAdvice
를 통해서 공통으로 Exception을 Handling 하고 있던 상황이라,
정상적으로 Exception Handling이 수행되고 있다고 생각하고 있었다.
@RestControllerAdvice
public class ExceptionController {
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(ExceptionController.class);
private static final String DEFALUT_MESSAGE_AUTHENTICATION = "Authentication is not valid";
@Autowired
private ExceptionService exceptionService;
@ExceptionHandler(AuthenticationException.class)
protected ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException e,
HttpServletRequest request) {
exceptionService.errorLog(e, request);
String message = e.getMessage() == null ? DEFALUT_MESSAGE_AUTHENTICATION : e.getMessage();
return new ResponseEntity<>(new ErrorResponse(HttpStatus.UNAUTHORIZED.value(), message, request.getSession().getId()),
HttpStatus.UNAUTHORIZED);
}
}
하지만 위와 같이 해당 Interceptor 단에서 throw 하던 Exception들은,
@ControllerAdvice
단에서 Handling 하지 못하고 있었고,
결국에는 preHandle()
단에서 throw
가 아닌 return false
로 handler로 전달되지 않는 Logic으로 변경하였다.
오늘은 이를 해결하는 동안 파악한 Exception Handling 관련 내용들을 gillog 한다.
먼저 아래 그림을 통해 Spring에서 MVC Request의 Lifecycle 과정을 확인해보면
🙈[Spring] Interceptor (1) - 개념 및 예제🐵[victorydntmd.tistory.com]
HandlerInterceptor
단은 Handler(Controller)
수행 전에,
DispatcherServlet
에게 Request
를 위임받아 처리하는 객체이다.
@ControllerAdvice
는 Handler(Controller)단에서 발생하는 Exception을,
@ExceptionHandler
Annotation으로 처리하는 Logic이라 Exception Handling이 불가능한 상황이었다.
원래 의도했던 Logic이 해당 Handler로 접근하기 전 올바른 권한을 확인 하고,
정상 권한이 없는 상황에서 접근 제한 용도로 작성했던 throw AuthenticationException()
Logic이라,
단순히 preHandle()
에서 return false
를 통해 해당 Handler로 접근을 제한하는 방법으로 변경했다.
아래 내용 부터는 Spring에서 Exception Handling
이 처리되는 과정을,
이번 기회를 통해 알게된 사항들을 작성한 부분이다.
Handler(Controller)
에서 발생한 Exception을 Handling 하기 위해서는,
HandlerExceptionResolver
의 Exception Handling 전략을 사용해야 한다.
HandlerExceptionResolver
는 Controller의 작업 중 발생한 Exception을 처리하는 결정 전략 객체이다.
Handler(Controller)
나 그 뒤 계층에서 throw된 Exception은,
DispatcherServlet
이 일단 전달 받은 뒤,
다시 Servlet 밖의 Servlet Container가 처리하게 되는데,
HandlerExcetpionResolver
가 등록되어 있지 않다면,
HTTP Status 500 내부 서버 오류
와 같은 메시지가 브라우저 단에 노출된다.
하지만 HandlerExceptionResolver
가 등록되어 있는 경우,
DispatcherServlet
은 먼저 HandlerExceptionResolver
가 해당 Exception을 Handling 할 수 있는지 확인하고,
Exception Handling이 가능한 경우 해당 HandlerExceptionResolver
에게 Exception Handling을 위임한다.
HandlerExceptionResolver
를 구현하기 위해서는 아래 Interface를 implements하면 되는데,
public interface HandlerExceptionResolver {
/**
* Try to resolve the given exception that got thrown during handler execution,
* returning a {@link ModelAndView} that represents a specific error page if appropriate.
* <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
* to indicate that the exception has been resolved successfully but that no view
* should be rendered, for instance by setting a status code.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or {@code null} if none chosen at the
* time of the exception (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
* @return a corresponding {@code ModelAndView} to forward to,
* or {@code null} for default processing in the resolution chain
*/
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
구현해야 하는 resolveException()
method는 return type이 ModelAndView
로,
예외 노출에 사용될 View와 그 안에 포함될 내용을 담은 Model 객체를 return 하도록 작성해주면 된다.
@ExceptionHandler
를 통해서 Exception을 Handling 하는 전략도,
AnnotationMethodHandlerExcetpionResolver
방식으로,
네 가지 HandlerExceptionResolver
구현 전략 중 한 가지 이다.
ResponseStatusExceptionResolver
: Exception을 특정 HTTP 응답 상태 코드로 전환 해주는 전략
DefaultHandlerExceptionResolver
: 404(NoSuchRequestHandlingMethodException)
, 400(TypeMissmatchException)
등 기본 Spring에서 마지막으로 Exception을 Handling 해주는 표준 예외처리 전략
SimpleMappingExceptionResolver
: web.xml
의 <error-page>
등으로 선언하는 예외 처리 view 지정 전략
그렇다면 HandlerInterceptor
단에서 Exception을 어떻게 처리할 수 있을까
그나마 접근한 방법은 HandlerInterceptor
단에서 preHandle()
method에서,
해당 Request
가 가고자하는 handler가 아닌,
Exception 처리를 관장하는 Handler로 Request를 Forward 시키는 방법이다.
아래 코드를 살펴보자.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws AuthenticationException {
if (request.getMethod().equals("OPTIONS")) {
return true;
}
String jwt = request.getHeader("Authorization");
if (jwt == null) {
LOG.error("[JwtInterceptor] JWT is null");
request.setAttribute("message", "JWT is required");
request.setAttribute("exception", "AuthenticationException");
request.getRequestDispatcher("/error").forward(request, response);
}
return true;
}
위 코드와 같이 HandlerInterceptor
단에서 예외상황 발생시,
/error
와 같은 Handler(Controller)
단으로 request를 forward
시킨 후,
@GetMapping("/error")
@ResponseBody
public void handlerError(HttpServletRequest request) throws Exception {
String message = (String) request.getAttribute("message");
String exception = (String) request.getAttribute("exception");
if(exception.equals("AuthenticationException")){
throw new AuthenticationException(message);
} else {
throw new Exception(message);
}
}
Handler(Controller)
를 등록하여 @ControllerAdvice
단에서 Exception을 Handling 할 수 있도록 처리하는 방법이다.
위 방법으로 처리하는것 보다,
예외 상황 발생 시 Interceptor 단에서 그냥 Handler로의 접근을 return false
로 끊어버리는게 더 올바른 방법이라 생각하여,
해당 방법은 실제로 사용하지는 않았다.
만약 비슷한 고민을 가지고 이 글을 읽고 계신 분이 계시다면,
더 현명한 처리 전략에 대해 고민 하실 수 있기를 바라며 gillog를 마친다. 🙋♂️