[spring] spring security의 예외처리

sujin·2023년 11월 26일
0

spring

목록 보기
11/13
post-thumbnail

Exception 구조화를 하기 위해, 보통 ExceptionHandler Class를 사용해서 예외처리를 진행한다. ExceptionHandler Class는 @RestControllerAdvice와 @ExceptionHandler를 통해서 만들 수 있다. 그러나 spring security에서 예외처리를 위해서도 해당방법을 사용해도 될까?

@RestControllerAdvice와 @ExceptionHandler는 Controller에서 동작

두개의 annotation을 사용해서 Application 전역에서 발생하는 특정 예외에 대해 , 예외를 한번에 처리할 수 있다.

  • @RestControllerAdvice
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를 만들면 됩니다~

  • 저의 경우, MyCustomException Class를 만들고 RuntimeException을 상속받아 super를 통해 생성자에 message를 함께 넣어 처리하였습니다. MyCustomException의 생성자에서 Trowable 생성자를 호출하기 위해, RuntimeException 부모 클래스의 기본 생성자를 사용해 message를 넘겨줬습니다. Java에서 모든 예외 클래스는 최소한 하나의 부모 클래스인 Throwable의 생성자를 호출해야합니다.! 해당 부분을 기억해서 만들면 될 것 입니다..! 만약 그게 아니라면 Compile Error가 날 것입니다.

다시 돌아와서 전역에서 발생하는 예외를 Controller단에서 처리한다? 그러면 Controller에 들어오기 전에 발생하는 예외는 어떻게 해야할까?를 생각해야합니다.

Spring Security에서 @ControllerAdvice를 사용하지 못하는 이유

결론부터 말하자면, 위의 방법인 "@RestControllerAdvice와 @ExceptionHandler를 사용해서는 spring filter chain에서의 Exception을 처리할 수 없습니다."

따라서, try-catch로 처리되고 있는 예외가 아닌 예상하지 못한 error가 발생했다면? Exception이 발생할 것이다.

그 이유는 spring security filterchain의 동작에 대해서 알면 자연스럽게 알게될 것이다.!

  • 사실 저도, ㅎㅎㅎㅎ 깊게 생각 하지 않고 코드를 짰고 accessToken이 유효하지 않았을 때 spring security filterchain에서 error가 났고 이를 대비하지 않아서 배포서버에서 제가 정의해둔 Custom Exception Response와 다른 구조의 error를 볼 수 있었습니다.허허허,, 그래서 서비스 실사용을 미루게 됐습니다...

spring security로 인증,인가 구현하기

해당 포스트에서 Spring Security를 공부하였습니다.

이 사이에 spring security를 사용한다면 filterchain이 연결되어 request를 가지고 dispatcher servlet으로 들어오고 -> 다시, servlet에서 response를 가지고 출발하여 filterchain을 거쳐서 다시 client에 나가는 구조입니다.

filter단에서 처리해줘야한다.

1. 인증 필터 : exceptionHandling과 accessDeniedHandler

.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler()) // 권한이 없는 경우의 처리

filterchain을 config를 작성할때, exceptionHandling을 사용해서 권한 에러에 대해서 처리를 진행해줬다!

  • accessDeniedHandler
    403 access Denied error를 핸들링 하기 위해서 만든 handler이다.

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));
        };
    }
    
  • 결과 (현재 Token에 대한 결과)
  • 추가로 알아두면 좋은 것!
    +) 추가로, 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을 하나 만들기로 했다!

2. 인가 필터

spring security fitler chain으로 만들어줬다! role의 경우에는 GrantedAuthority를 확인하는거고 Authentication이 성공한 시점에 Authorization을 하는 것이다!

이때, 인증은 사실 Role에 의해서 Forbidden만 존재하기에 위의 방법으로 간편하게 처리했다! 반면 Authentication은 그 안에서 발생가능한 예외가 많다.

token이 없을 때, 토큰이 유효하지 않은 경우. 등등이 존재한다.

로직

  • spring security는 doFilter()로 다음단계 Filter로 넘긴다.
    Filter1 -> Filter2( 인증필터 : 여기서 authentication 예외발생 가능) -> Filter1(예외처리) 다음과 같이 동작해야한다.
    따라서, 여기서 Filter1은 예외처리를 진행해줘야하고 처리해주는 로직은 Filter2가 될 것이기에 Filter1이 ExceptionFilterChain이 되어야하고 Filter2는 인증,인가를 위한 FilterChain이 될 것이다.

나의 코드에서는 Filter2의 역할을 하는것은 jwtAuthenticationFilter이다.
jwtAuthenticationFilter 이전에 Filter1의 역할을 하는 jwtAuthenticationExceptionFilter가 동작하도록 하기 위해서, addFilterBefore를 사용하면 된다.

.addFilterBefore(jwtAuthenticationFilter, RequestHeaderAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationExceptionFilter, JwtAuthenticationFilter.class); // 인가에 대한 필터

jwtAuthenticationExceptionFilter 생성

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이 없는 경우와 유효하지 않은 경우 등등에 대해서 처리해줄 수 구조화된 예외처리를 해줄 수 있었다!

0개의 댓글