🔐 Spring Security 를 이용해서 토큰을 검증하고 인증된 사용자 정보를 담은 객체를 생성했다. 이 과정에서 토큰이 유효하지 않은 경우, 예외처리를 하려고 한다. 예외처리를 어떻게 할 수 있는지 찾아보자!
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에서 예외처리를 해줄 수 없는 것이다.
두번째로 시도한 방법은 필터 내에서 예외처리를 다루는 것이었다. 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에 설정할 수 있는 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에 잘 담겨 보내지는 것을 확인할 수 있다.