[Spring Security] Filter 예외처리는 어떻게 할까?

Doyeon·2023년 2월 21일
9
post-thumbnail
post-custom-banner

🔐 Spring Security 를 이용해서 토큰을 검증하고 인증된 사용자 정보를 담은 객체를 생성했다. 이 과정에서 토큰이 유효하지 않은 경우, 예외처리를 하려고 한다. 예외처리를 어떻게 할 수 있는지 찾아보자!

첫번째 시도 - @ControllerAdvice, @ExceptionHandler

REST API 예외를 처리하기 위해 많이 사용하는 방법은 @ControllerAdvice, @ExceptionHandler를 이용하여 Global Exception을 처리하는 것이다. Exception마다 어떤 처리를 할지 정해줄 수 있고 비지니스 로직으로부터 예외처리 로직을 분리해낼 수 있어 객체지향 관점에서도 괜찮은 방법이다.

Security Filter에서도 토큰이 유효하지 않을 경우, exception을 발생시켜 기존에 만들어놓은 CustomExceptionHandler 에서 예외처리하도록 시도해보았다.

코드

/* JwtAuthFilter */

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // request 에 담긴 토큰을 가져온다.
    String token = jwtUtil.resolveToken(request);

    // 토큰이 null 이면 다음 필터로 넘어간다.
    if (token == null) {
        filterChain.doFilter(request, response);
        return;
    }

    // 토큰이 유효하지 않으면 예외처리
    if (!jwtUtil.validateToken(token)) {
        **throw new JwtException(ErrorType.NOT_VALID_TOKEN);**
    }

    // 유효한 토큰이라면, 토큰으로부터 사용자 정보를 가져온다.
    Claims info = jwtUtil.getUserInfoFromToken(token);
    setAuthentication(info.getSubject());   // 사용자 정보로 인증 객체 만들기

    // 다음 필터로 넘어간다.
    filterChain.doFilter(request, response);

}
/* JwtException */

@Getter
public class JwtException extends RuntimeException {
    private final ErrorType errorType;

    public JwtException(ErrorType errorType) {
        this.errorType = errorType;
    }
}
/* CustomExceptionHandler */

@ExceptionHandler(JwtException.class)
public ResponseEntity<MessageResponseDto> customException(JwtException e) {
    MessageResponseDto responseDto = makeErrorResponse(e.getErrorType());
    return ResponseEntity.badRequest().body(responseDto);
}

private MessageResponseDto makeErrorResponse(ErrorType error) {
    return MessageResponseDto.of(error.getCode(), error.getMessage());
}
/* ErrorType */
@Getter
public enum ErrorType {
    NOT_VALID_TOKEN(400, "토큰이 유효하지 않습니다."),
    NOT_WRITER(400, "작성자만 삭제/수정할 수 있습니다."),
    DUPLICATED_USERNAME(400, "중복된 username 입니다."),
    NOT_MATCHING_INFO(400, "회원을 찾을 수 없습니다."),
    NOT_MATCHING_PASSWORD(400, "비밀번호가 일치하지 않습니다."),
    NOT_FOUND_USER(400, "사용자가 존재하지 않습니다."),
    NOT_FOUND_WRITING(400, "게시글/댓글이 존재하지 않습니다.");

    private int code;
    private String message;

    ErrorType(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

결과

에러 메시지와 상태코드를 담아 클라이언트로 보낼거라는 예상과는 달리 JwtException 자체가 제대로 동작하지 않은 것을 볼 수 있다.

찾아보니, 핸들러를 사용한 예외처리는 DispatcherServlet에 의해 처리되는 경우 작동한다고 한다.

Security Filter는 DispatcherServlet에 도달하기 전에 먼저 거쳐지는 곳이기 때문에 @ControllerAdvice에서 예외처리를 해줄 수 없는 것이다.

두번째 시도 - 필터 내 예외처리(ResponseEntity 반환)

두번째로 시도한 방법은 필터 내에서 예외처리를 다루는 것이었다. API 요청 중 Exception이 발생했을 때, ResponseEntity의 status에는 상태코드를, body에는 클라이언트에서 볼 수 있는 상태코드, 메시지가 담긴 DTO를 담아서 리턴해주는 방법을 많이 사용했었다. 똑같이 적용하면 되겠지? 생각하고 다음과 같이 시도해봤다.

코드

// 토큰이 유효하지 않으면 예외처리
if (!jwtUtil.validateToken(token)) {
    jwtExceptionHandler(response, ErrorType.NOT_VALID_TOKEN);
    return;
}

public ResponseEntity<MessageResponseDto> jwtExceptionHandler(ErrorType error) {
      MessageResponseDto dto = MessageResponseDto.of(error.getCode(), error.getMessage());
      return ResponseEntity.badRequest().body(dto);
  }

결과

Client쪽에는 아무것도 뜨지 않고, Status값도 200으로 나와버렸다..

다시 생각해보면 지금은 Servlet에 도달하기 전인 Filter에 있기 때문에, DTO나 ResponseEntity를 필터 내에서 리턴할 수 있는 구조가 아니다.

Servlet에 도달하기 전에 사용할 수 있는 HttpServletResponse를 이용하여 클라이언트에게 보여질 내용을 담아야 한다.

세번째 시도<성공> - 필터 내 예외처리(HttpServletResponse)

필터 내에서 예외처리를 하되, HttpServletResponse를 이용해 클라이언트에 보여질 내용을 담는다.

HttpServletResponse에 설정할 수 있는 status, contentType, cahracterEncoding 등을 설정하고, 클라이언트로 보내고 싶은 DTO를 String으로 변환하여 HttpServletResponse에 담는다.

코드

// 토큰이 유효하지 않으면 예외처리
if (!jwtUtil.validateToken(token)) {
    jwtExceptionHandler(response, ErrorType.NOT_VALID_TOKEN);
}

// 토큰에 대한 오류가 발생했을 때, 커스터마이징해서 Exception 처리 값을 클라이언트에게 알려준다.
public void jwtExceptionHandler(HttpServletResponse response, ErrorType error) {
    response.setStatus(error.getCode());
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    try {
        String json = new ObjectMapper().writeValueAsString(MessageResponseDto.of(error.getCode(), error.getMessage()));
        response.getWriter().write(json);
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}

결과

Status도 400 Bad Request이고, 클라언트에게 보여줄 메시지와 상태코드도 Body에 잘 담겨 보내지는 것을 확인할 수 있다.

profile
🔥
post-custom-banner

0개의 댓글