filter에서 exception handling하는 법

김가빈·2023년 12월 26일
0

springsecurity

목록 보기
19/23
  • spring seucurity로 로그인 시 tokenException이 발생하면 유저에게 error정보를 제공해 줘야 하는 상황이었다.
  • 기존에 다음과 같이 GloablExceptionHandler를 작성해 두어서 당연히 tokenException을 잡아서 처리해 줄 것이라고 생각했으나, 서버의 에러상태가 가공되지 않고 그대로 전달되는 상황이 발생했다.
package com.project.bookforeast.common.domain.error;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import com.project.bookforeast.common.security.error.TokenErrorResult;
import com.project.bookforeast.common.security.error.TokenException;
import com.project.bookforeast.user.error.UserErrorResult;
import com.project.bookforeast.user.error.UserException;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

	// 클라이언트에서 파라미터 잘못 전달한 경우
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(
			MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

		// exception으로 부터 에러를 가져와서 list에 담는다.
		final List<String> errorList = ex.getBindingResult()
										 .getAllErrors()
										 .stream()
										 .map(DefaultMessageSourceResolvable::getDefaultMessage)
										 .collect(Collectors.toList());
										 
		// 해당 에러메세지 로그를 찍는다.
		log.warn("클라이언트로부터 잘못된 파라미터 전달됨 : {}", errorList);
		return ResponseEntity.status(HttpStatus.BAD_REQUEST)
				.body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), errorList.toString()));		
	}

	
	// 사용자 정의 excepion이 발생한 경우
	// userException
	@ExceptionHandler({UserException.class})
	public ResponseEntity<ErrorResponse> handleUserException(final UserException exception) {
		log.warn("UserException occur:" + exception);
		UserErrorResult errorResult = exception.getUserErrorResult();
		return ResponseEntity.status(errorResult.getStatus())
				.body(new ErrorResponse(errorResult.getStatus().value(), errorResult.getMessage()));
		
	}
	
	// tokenException
	@ExceptionHandler({TokenException.class})
	public ResponseEntity<ErrorResponse> handleTokenException(final TokenException exception) {
		log.warn("TokenException occur:" + exception);
		TokenErrorResult errorResult = exception.getTokenErrorResult();
		return ResponseEntity.status(errorResult.getStatus())
				.body(new ErrorResponse(errorResult.getStatus().value(), errorResult.getMessage()));
	}
	

	
	@RequiredArgsConstructor
	@Getter
	static class ErrorResponse {
		private final int code;
		private final String message;
	}
	
}

그 이유는 @RestControllerAdvice때문이었다. 이 어노테이션으로 인해서 이 globalExceptionHandler는 restController의 로직을 타는 중 발생하는 에러만 처리해 주고 있었다.

해결방법

  • jwt유효성 검사 이전에 filter에서 발생한 exception을 처리해 줄 exception처리 filter를 만든다.
  • 해당 filter에서 발생하는 tokenException을 잡아서 유저에게 전달할 error메세지를 직접 custom한다.
package com.project.bookforeast.common.domain.error;

import java.io.IOException;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.bookforeast.common.domain.error.GlobalExceptionHandler.ErrorResponse;
import com.project.bookforeast.common.security.error.TokenErrorResult;
import com.project.bookforeast.common.security.error.TokenException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {
			filterChain.doFilter(request, response);
		} catch (TokenException tokenException) {
			log.warn("TokenException occur:");
			TokenErrorResult errorResult = tokenException.getTokenErrorResult();
			setErrorResponse(errorResult.getStatus(), errorResult.getMessage() ,response);
		}
	}

	private void setErrorResponse(HttpStatus status, String message, HttpServletResponse response) {
		response.setStatus(status.value());
		response.setContentType("application/json");
		response.setCharacterEncoding("UTF-8");
		ErrorResponse errorResponse = new ErrorResponse(status.value(), message);
		
		try {
			String json = new ObjectMapper().writeValueAsString(errorResponse);
			response.getWriter().write(json);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}


}
package com.project.bookforeast.common.security.error;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class TokenException extends RuntimeException {

	private final TokenErrorResult tokenErrorResult;
}
package com.project.bookforeast.common.security.error;

import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum TokenErrorResult {

	TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."),
	ACCESS_TOKEN_NEED(HttpStatus.UNAUTHORIZED, "엑세스 토큰이 필요합니다."),
	REFRESH_TOKEN_NEED(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 필요합니다."),
	TOKEN_EMPTY(HttpStatus.UNAUTHORIZED, "토큰을 전달해주세요")
	;
	
	private final HttpStatus status;
	private final String message;
}
  • 그리고 spring security에서 이 exception handler filter가 jwt유효성 검사 filter보다 우선적으로 돌아야 하므로 다음과 같이 추가해준다.

  • 그 후 filter에서 tokenException을 발생시켜보면 다음과 같이 성공적으로 custom메세지가 뜨는 것을 확인할 수 있다.
profile
신입 웹개발자입니다.

0개의 댓글