본문 바로가기

Backend

[Spring Security] FilterChain, ExceptionHandler 커스터마이징2

반응형

 

이전 글에서 accessDeniedHandler를 커스텀하여 예외처리 응답을 커스텀하는 과정을 수행했다. 

이후에, 토큰 만료 테스트를 시도했는데, 어찌된 영문인지 CustomAuthenticationEntryPoint 클래스에 작성해둔 사용자 인증 예외 프로세스가 동작을 안하고, Servlet 이전 Filter에서 에러를 캐치하여 먼저 에러메시지를 반환하는 결과를 마주했다. 

 

SecurityConfig 즉 ContextHolder에서 예외처리 custom 핸들러를 주입했지만, CustomAuthenticationEntryPoint가 실행이 안되었다. 여러 차례 다른 블로그에서 수행하는 방법으로 시도했지만, AuthenticationEntryPoint 인터페이스를 구체화하는 과정을 포기하기로 했다. 대신 chanin 내부에서 doFilter를 수행하면서 exception이 발생하면 곧장 예외를 캐치하여 응답메시지를 보내기로 했다.

 

doFilter를 실행하면서 exception을 잡아내기 위해 chainfilter 과정 중 JwtAuthFilter class 이전에 수행시킬 JwtExceptionFilter class를 하나 더 추가했다.

 

// SecurityConfig 일부
		...
 		// JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
        // JwtExceptionFilter를 JwtAuthFilter를 앞에 추가
        http.addFilterBefore(new JwtAuthFilter(customUserDetailService, jwtUtil), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(new JwtExceptionFilter(objectMapper), JwtAuthFilter.class);
        http.exceptionHandling( (exceptionHandling) -> exceptionHandling
//                .authenticationEntryPoint(new CustomAuthenticationEntryPoint(customAuthenticationEntryPoint))
                .accessDeniedHandler(customAccessDeniedHandler));

 

그리고 JwtExceptionFilter class 내에서 곧장 client로 응답을 보낼 수 있게 코드를 작성했다.

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtExceptionFilter extends OncePerRequestFilter { // OncePerRequestFilter : 한 번 실행 보장

    private final ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (JwtException ex) {
            String message = ex.getMessage();
            System.out.println(message);
            if(ErrorMessage.UNKNOWN_ERROR.getMsg().equals(message)) {
                setResponse(response, ErrorMessage.UNKNOWN_ERROR);
            }
            //잘못된 타입의 토큰인 경우
            else if(ErrorMessage.WRONG_TYPE_TOKEN.getMsg().equals(message)) {
                setResponse(response, ErrorMessage.WRONG_TYPE_TOKEN);
            }
            //토큰 만료된 경우
            else if(ErrorMessage.EXPIRED_TOKEN.getMsg().equals(message)) {
                setResponse(response, ErrorMessage.EXPIRED_TOKEN);
            }
            //지원되지 않는 토큰인 경우
            else if(ErrorMessage.UNSUPPORTED_TOKEN.getMsg().equals(message)) {
                setResponse(response, ErrorMessage.UNSUPPORTED_TOKEN);
            }
            else {
                setResponse(response, ErrorMessage.ACCESS_DENIED);
            }
        }
    }

    public void setResponse(HttpServletResponse response, ErrorMessage errorMessage) throws RuntimeException, IOException {
        ErrorMessageDto responseMsg = new ErrorMessageDto(errorMessage.getCode(), errorMessage.toString(), errorMessage.getMsg());
        String responseBody = objectMapper.writeValueAsString(responseMsg);
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(errorMessage.getCode());
        response.getWriter().write(responseBody);
    }
}

 

 

이렇게 코드를 작성할 경우 JwtExceptionFilter - JwtAuthFilter 순으로 진행되기 때문에 예외처리 뿐만 아니라 토큰 검증까지도 원활하게 수행할 수 있다.

 

이전 글에서 작성했던, HandlerExceptionResolver는 사용하지 않기로 했다. 

 

 

<테스트 결과>

- admin만 접근 가능한 api에 요청했을 때,

{
    "message": "접근이 거부되었습니다.",
    "statusCode": 403,
    "statusMessage": "ACCESS_DENIED"
}

 

- 토큰이 만료되었을 때,

{
    "message": "토큰이 만료되었습니다.",
    "statusCode": 401,
    "statusMessage": "EXPIRED_TOKEN"
}
 
 

<ErrorMessage 정의, messageResponseDto,  CustomAccessDeniedHandler 재정의>

더보기
public enum ErrorMessage {
    UNKNOWN_ERROR("알 수 없는 오류가 발생했습니다.", 500),
    WRONG_TYPE_TOKEN("잘못된 타입의 토큰입니다.", 400),
    EXPIRED_TOKEN("토큰이 만료되었습니다.", 401),
    UNSUPPORTED_TOKEN("지원되지 않는 토큰입니다.", 400),
    ACCESS_DENIED("접근이 거부되었습니다.", 403);

    private final String msg;
    private final int code;

    ErrorMessage(String msg, int code) {
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public int getCode() {
        return code;
    }
}
@AllArgsConstructor
@Getter
public class ErrorMessageDto {

    private int StatusCode;
    private String StatusMessage;
    private String message;


}
/** 인증은 되었지만 특정 리소스에 대한 권한이 없는 경우 호출되는 핸들러 */
@Component
@Slf4j(topic = "FORBIDDEN_EXCEPTION_HANDLER")
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws ServletException, IOException{
        log.error(accessDeniedException.getMessage());

        ErrorMessageDto responseMsg = new ErrorMessageDto(ErrorMessage.ACCESS_DENIED.getCode(), ErrorMessage.ACCESS_DENIED.toString(), ErrorMessage.ACCESS_DENIED.getMsg());
        String responseBody = objectMapper.writeValueAsString(responseMsg);
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(ErrorMessage.ACCESS_DENIED.getCode());
        response.getWriter().write(responseBody);
    }
}

이제는 refresh토큰을 적용하고자 한다.

반응형