Exception 구조화를 하기 위해, 보통 ExceptionHandler Class를 사용해서 예외처리를 진행한다. ExceptionHandler Class는 @RestControllerAdvice와 @ExceptionHandler를 통해서 만들 수 있다. 그러나 spring security에서 예외처리를 위해서도 해당방법을 사용해도 될까?
두개의 annotation을 사용해서 Application 전역에서 발생하는 특정 예외에 대해 , 예외를 한번에 처리할 수 있다.
A convenience annotation that is itself annotated with @ControllerAdvice and @ResponseBody.
Types that carry this annotation are treated as controller advice where @ExceptionHandler methods assume @ResponseBody semantics by default.
해당 annotation은 @ControllerAdvice와 @ResponseBody로 구성되어있는데, @ControllerAdvice의 역할은 아래의 사진과 같다.
@ControllerAdvice는 여러 @Controller 클래스에 걸쳐 코드를 중복하지 않고 예외 처리 및 기타 관련 작업을 효과적으로 공유하고자 할 때 사용되도록 구현된다.
여러 @Controller 클래스 간에 공유되는 @ExceptionHandler, @InitBinder, 또는 @ModelAttribute 메서드를 선언하는 클래스를 위한 @Component의 특수화된 어노테이션이다.
즉, @ControllerAdvice가 붙여진 @RestControllerAdvice는 @ExceptionHandler를 @ResponseBody 의미를 가진다.
따라서, Controller 클래스에 걸쳐서 코드 중복을 피하기 위해서 특정 예외들에 대해서 한번에 처리하고자, CustomExceptionHandler를 생성하기 위해 두 annotation을 사용한다.
어떻게 사용할 수 있나?
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
// 예외에 대한 처리 로직
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Internal Server Error");
}
@ExceptionHandler(MyCustomException.class)
public ResponseEntity<String> handleCustomException(MyCustomException e) {
// 특정 예외에 대한 처리 로직
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Bad Request: " + e.getMessage());
}
}
다음과 같이 ExceptionHandler를 만들 수 있게 되는 것이다.
위의 코드는 단순히 예시를 보여줄 뿐입니다! 각자 원하는 MyCustomException Class를 만들면 됩니다~
다시 돌아와서 전역에서 발생하는 예외를 Controller단에서 처리한다? 그러면 Controller에 들어오기 전에 발생하는 예외는 어떻게 해야할까?를 생각해야합니다.
결론부터 말하자면, 위의 방법인 "@RestControllerAdvice와 @ExceptionHandler를 사용해서는 spring filter chain에서의 Exception을 처리할 수 없습니다."
따라서, try-catch로 처리되고 있는 예외가 아닌 예상하지 못한 error가 발생했다면? Exception이 발생할 것이다.
그 이유는 spring security filterchain의 동작에 대해서 알면 자연스럽게 알게될 것이다.!
해당 포스트에서 Spring Security를 공부하였습니다.
이 사이에 spring security를 사용한다면 filterchain이 연결되어 request를 가지고 dispatcher servlet으로 들어오고 -> 다시, servlet에서 response를 가지고 출발하여 filterchain을 거쳐서 다시 client에 나가는 구조입니다.
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler()) // 권한이 없는 경우의 처리
filterchain을 config를 작성할때, exceptionHandling을 사용해서 권한 에러에 대해서 처리를 진행해줬다!
AccessDeniedHandler를 넘겨줘야한다.
따라서, 필요한 parameter을 넘겨 custom handler를 생성해줘서 AccessDeniedHandler를 반환하도록 해줬다.
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
CustomException customException = new ForbiddenException("forbidden",this.getClass().toString());
ErrorResponse errorResponse =
new ErrorResponse(customException.getErrorType(), customException.getMessage(), customException.getPath());
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("status", "FAILED");
responseBody.put("data", errorResponse);
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
};
}
추가로 알아두면 좋은 것!
+) 추가로, unauthorized에 대해서도 처리할 수 있다. (사용할게 아니라 간략히만 작성했다!)
AuthenticationEntryPoint
Commences an authentication scheme.
여기서 볼 수 있듯이, request, response, exception을 parameter로 넘겨줘야한다.lambda 표현식을 사용한다면 아래와 같이 만들 수 있을 것이다!
@Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
return (request, response, authException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
-> 하지만, 다양한 에러들을 처리해주기 위해서 FilterChain을 하나 만들기로 했다!
spring security fitler chain으로 만들어줬다! role의 경우에는 GrantedAuthority를 확인하는거고 Authentication이 성공한 시점에 Authorization을 하는 것이다!
이때, 인증은 사실 Role에 의해서 Forbidden만 존재하기에 위의 방법으로 간편하게 처리했다! 반면 Authentication은 그 안에서 발생가능한 예외가 많다.
token이 없을 때, 토큰이 유효하지 않은 경우. 등등이 존재한다.
로직
나의 코드에서는 Filter2의 역할을 하는것은 jwtAuthenticationFilter이다.
jwtAuthenticationFilter 이전에 Filter1의 역할을 하는 jwtAuthenticationExceptionFilter
가 동작하도록 하기 위해서, addFilterBefore
를 사용하면 된다.
.addFilterBefore(jwtAuthenticationFilter, RequestHeaderAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationExceptionFilter, JwtAuthenticationFilter.class); // 인가에 대한 필터
CustomException에 해당하는 에러가 'jwtAuthenticationFilter'에서 발생했다면, 그 error message를 전송할 것이다.
그 외의 예외에 대해서는 500 interval error message를 전송해줄 것이다.
instanceOf
instanceOf를 사용해서 Exception의 종류를 구분하여 처리하면 된다.
@Component
@Slf4j
public class JwtAuthenticationExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try{
filterChain.doFilter(request, response); // jwtAuthentication 실행
}catch(Exception e){
if (e instanceof CustomException) {// 정의한 error에 속할 경우
CustomException customException = (CustomException) e;
handleAuthenticationException(response, customException.getMessage());
}else{
handleAuthenticationException(response, "internal server error"); // 전체 internal로 처리
}
}
}
private void handleAuthenticationException(HttpServletResponse response,String message ) throws IOException {
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> responseBody = new HashMap<>();
Map<String, Object> responseBodyData = new HashMap<>();
responseBodyData.put("message", message);
responseBody.put("status", "FAILED");
responseBody.put("data", responseBodyData);
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(responseBody));
writer.flush();
writer.close();
}
}
나는 모든 Response를 200 status로 설정하고 status로 구분하는 틀을 잡아놨기에, Spring Security의 Excpetion의 Response도 그 구조에 맞춰서 제공해주기 위해서 handleAuthenticationException 내부에서 Response 위와같이 작성하였다.
이렇게 Filter Chain을 Custom해서 token이 없는 경우와 유효하지 않은 경우 등등에 대해서 처리해줄 수 구조화된 예외처리를 해줄 수 있었다!