[ERROR] 필터 예외 처리 (Feat : JWT)

2024. 8. 29. 17:53·ETC/ERROR

😂 ISSUE


 

엑세스 토큰이 만료된 경우, 토큰이 만료되었다는 예외를 던지는데, 이런 Response가 나온다.

{
    "status": "UNAUTHORIZED",
    "code": 401,
    "message": "인증되지 않은 유저입니다.",
    "detail": null
}

 

인증되지 않은 유저도 맞지만, 난 아래의 형태처럼 더 적절한 예외 메시지를 리턴하고 싶다.

{
    "status": "UNAUTHORIZED",
    "code": 401,
    "message": "토큰이 만료되었습니다.",
    "detail": null
}
{
    "status": "UNAUTHORIZED",
    "code": 401,
    "message": "인증용 헤더가 비어있습니다.",
    "detail": null
}
{
    "status": "UNAUTHORIZED",
    "code": 401,
    "message": "잘못된 토큰 타입입니다.",
    "detail": null
}

 

🤔 REASON


 

토큰 만료 예외를 던지려고 한 곳은 필터에서 호출한 토큰 발급기의 토큰 검증 메서드 이다.

// 토큰 발급기
public class JwtTokenProvider {

    ...		

		// 토큰 검증 메서드
    public boolean validateToken(String jwtToken) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return true;
        } catch (IllegalArgumentException e) {
            log.error("헤더가 비어있음");
            throw new RestApiException(CommonErrorCode.EMPTY_AUTHORIZATION_HEADER, MethodInfoUtil.getDetail());
        } catch(ExpiredJwtException e) {
            log.error("Token 만료");
            throw new RestApiException(CommonErrorCode.EXPIRED_ACCESS_TOKEN, "토큰 만료");
        } catch(JwtException e) {
            log.error("잘못된 Access Token 타입");
            throw new RestApiException(CommonErrorCode.WRONG_TOKEN_TYPE, MethodInfoUtil.getDetail());
        }

        return false;
    }
}

 

 

그런데 validateToken() 메서드에서 예외를 던지면 JwtAuthenticationEntryPoint 의 commence() 메서드가 예외를 처리한다.

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        setResponse(response);
    }

    private void setResponse(HttpServletResponse response) throws IOException {
        ErrorResultDto errorResultDto = new ErrorResultDto(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "인증되지 않은 유저입니다.");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(errorResultDto));
    }
}

 

 

저 setResponse() 메서드 때문에 응답 형태가 다음과 같은 것 같다.

{
    "status": "UNAUTHORIZED",
    "code": 401,
    "message": "인증되지 않은 유저입니다.",
    "detail": null
}

 

그럼 Spring Security에서, 토큰을 관리하는 과정에서 JwtAuthenticationEntryPoint 가 어디에 사용되고, 목적이 뭘까?

 

 

JwtAuthenticationEntryPoint

 

인증에 실패한 사용자에 대한 응답을 위해 사용되는데, 내가 구현한 형태는 HttpStatus.UNAUTHORIZED 상태를 응답하도록 되어있다. 주로 401 에러를 처리하기 위해 사용한다. 또한 WebSecurityConfig 를 보면, JwtAuthenticationEntryPoint 는 filterChain() 메서드에서 설정되고 사용된다는 것을 알 수 있다.

public class WebSecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 1. csrf 설정
        http.csrf(AbstractHttpConfigurer::disable);

        // 2. 권한에 따른 보안 예외 처리 설정
        http.exceptionHandling(e -> e.authenticationEntryPoint(jwtAuthenticationEntryPoint)
		        .accessDeniedHandler(jwtAccessDeniedHandler));

        ...

        return http.build();
    }
}

 

결국 JwtAuthenticationEntryPoint도 필터에서 사용되는 것이다.

 

하지만 내가 사용한 공통 예외 처리(@ControllerAdvice)는 디스패처 서블릿 에 붙어있는 기술이다.

 

이 점을 명심하고 대략적인 사용자 인증의 예외 처리 흐름을 파악해보면 아래와 같다.

 

공통 예외 처리기인 GlobalExceptionHandler는 사진과 같이 디스패처 서블릿에 붙어있다.

 

그렇기 때문에 사용자 인증(토큰 인증)과 관련된 예외 처리는 필터에서 해줘야 한다.

 

 

😃 SOLVE


일단 JwtAuthenticationEntryPoint 에 있는 setResponse() 메서드는 주석처리 한다.

public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//        setResponse(response);
    }

    private void setResponse(HttpServletResponse response) throws IOException {
        ErrorResultDto errorResultDto = new ErrorResultDto(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "인증되지 않은 유저입니다.");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(errorResultDto));
    }
}

 

 

그리고 JwtTokenProvider 에서 jwtExceptionHandler() 메서드를 추가한다.

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    private final ObjectMapper objectMapper;
    private final UserDetailsService userDetailsService;

    ...
    
    public boolean validateToken(String jwtToken, HttpServletResponse response) throws IOException {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return true;
        } catch (IllegalArgumentException e) {
            log.error("헤더가 비어있음");
            jwtExceptionHandler(CommonErrorCode.EMPTY_AUTHORIZATION_HEADER, response);
        } catch(ExpiredJwtException e) {
            log.error("Token 만료");
            jwtExceptionHandler(CommonErrorCode.EXPIRED_ACCESS_TOKEN, response);
        } catch(JwtException e) {
            log.error("잘못된 Access Token 타입");
            jwtExceptionHandler(CommonErrorCode.WRONG_TOKEN_TYPE, response);
        }

        return false;
    }

    private void jwtExceptionHandler(CommonErrorCode wrongTokenType, HttpServletResponse response) throws IOException {
        ErrorResultDto errorResultDto = new ErrorResultDto(wrongTokenType.getHttpStatus(),
                wrongTokenType.getHttpStatus().value(),
                wrongTokenType.getMessage());

        response.setStatus(wrongTokenType.getHttpStatus().value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(errorResultDto));
    }
}

 

 

이렇게 코드를 수정&추가하면 필터에서도 예외 처리를 커스텀할 수 있다.

'ETC > ERROR' 카테고리의 다른 글

[ERROR] M1 맥북 Jmeter 실행 에러  (0) 2024.08.01
'ETC/ERROR' 카테고리의 다른 글
  • [ERROR] M1 맥북 Jmeter 실행 에러
오도형석
오도형석
  • 오도형석
    형석이의 성장일기
    오도형석
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • MSA 모니터링 서비스
        • DB
      • 스파르타 코딩클럽
        • SQL
        • Spring
      • 백엔드
        • Internet
        • Java
        • DB
      • 캡스톤
        • Django
        • 자연어처리
      • Spring
        • JPA
        • MSA
      • ETC
        • ERROR
      • 개발 일기 N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 인기 글

  • 태그

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
오도형석
[ERROR] 필터 예외 처리 (Feat : JWT)
상단으로

티스토리툴바