Spring & SpringBoot

Java/SpringBoot 게시판 기능 구현_Token 관련 예외 처리

예령 : ) 2023. 7. 5. 23:40
💡 개발 환경
Java 11, Spring 2.7.X, Gradle 7.5, MySQL

 

[구현하려는 기능]

1. 만료된 JWT token 으로 API 요청 시 401에러와 관련 에러 메세지 return

2. 비회원이 토큰이 필요한 API 요청 시 400에러와 관련 에러 메세지 return

3. @Vaild 예외 처리 시 response 형태 수정

 

[문제 상황]

1. 만료된 토큰으로 API 요청 시 postman에서는 500에러가 발생하는 반면, IntelliJ Run 창에는 던진 에러메세지가 출력됨.

 

[시도한 방법]

  1. 우선 현재 토큰을 검증하는 로직은 JwtAuthenticationFilter, TokenProvider 클래스와 같은 `filter` 단계에서 이루어지므로 `@RestControllerAdvice`가 있는 RestApiExceptionHandler 클래스에서는 처리가 불가능했습니다.
  2. filter단에서 예외 처리를 하기 위해 아래 처럼 수정하였습니다.

 


 

기존 코드

JwtAuthenticationFilter.java

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String token = tokenProvider.resolveToken(request);
    if (token != null) {
        Authentication authentication = tokenProvider.getAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
       }
    filterChain.doFilter(request, response);
}

 

TokenProvider.java

public Authentication getAuthentication(String token) {
    UserDetails userDetails = userDetailsService.loadUserByUsername(decodeUsername(token));
    return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

// 토큰에서 회원 정보 추출  
public String decodeUsername(String token) {  
    DecodedJWT decodedJWT = isValidToken(token)  
            .orElseThrow(() -> new CustomException(ErrorCode.ILLEGAL\_INVALID\_TOKEN));  

    Date now = new Date();  
    if (decodedJWT.getExpiresAt().before(now)) {  
        throw new CustomException(ErrorCode.ILLEGAL\_INVALID\_TOKEN));  
    }  

    return decodedJWT  
            .getClaim(JwtTokenUtils.CLAIM\_USERID)  
            .asString();  
}  

// 토큰 유효성 검사  
public Optional<DecodedJWT> isValidToken(String token) {  
    DecodedJWT jwt = null;  
    try {  
        JWTVerifier verifier = JWT  
                .require(generateAlgorithm(JWT\_SECRET))  
                .build();  
        jwt = verifier.verify(token);  
    } catch (TokenExpiredException e) {  
        logger.error(e.getMessage());  
    }  
    return Optional.ofNullable(jwt);  
}

 

 


수정한 코드

JwtAuthenticationFilter.java

 

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
   String token = tokenProvider.resolveToken(request);
   String method = request.getMethod();
   log.info("requestMethod : {}", method);
   String servletPath = request.getServletPath();
   if (token != null) {
       Authentication authentication = tokenProvider.getAuthentication(token, response); 
       SecurityContextHolder.getContext().setAuthentication(authentication);
      }
   // 토큰이 없는 경우에 회원만 가능한 API 요청 시 에러 처리, 추후 거래게시판
   else if (servletPath.equals("/api/v1/rehoming") && method.equals("POST") || servletPath.startsWith("/api/v1/likes") ||
            servletPath.startsWith("/api/v1/bookmarks") || servletPath.startsWith("/api/v1/rehoming/status")) {
       tokenProvider.tokenNullChk(response);
      }
   filterChain.doFilter(request, response);
}

 

 

TokenProvider.java

 

public Authentication getAuthentication(String token, HttpServletResponse response) throws IOException {
    UserDetails userDetails = userDetailsService.loadUserByUsername(decodeUsername(token, response));
    return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

// 토큰에서 회원 정보 추출  
public String decodeUsername(String token, HttpServletResponse response) throws IOException {  
    DecodedJWT decodedJWT = isValidToken(token, response)  
            .orElseThrow(() -> new IllegalArgumentException("유효한 토큰이 아닙니다."));  

    Date now = new Date();  
    if (decodedJWT.getExpiresAt().before(now)) {  
        throw new IllegalArgumentException("만료된 토큰");  
    }  

    return decodedJWT  
            .getClaim(JwtTokenUtils.CLAIM\_USERID)  
            .asString();  
}  

// 토큰 유효성 검사  
public Optional<DecodedJWT> isValidToken(String token, HttpServletResponse response) throws IOException {  
    DecodedJWT jwt = null;  
    try {  
        JWTVerifier verifier = JWT  
                .require(generateAlgorithm(JWT\_SECRET))  
                .build();  
        jwt = verifier.verify(token);  
    } catch (TokenExpiredException e) {  
        logger.error(e.getMessage());  
        tokenExpired(response);  
    }  

// 토큰이 null일 경우 error 처리  
public void tokenNullChk(HttpServletResponse response) throws IOException {  
    log.info("TokenProvider : JWT Token이 존재하지 않습니다.");  
    response.setStatus(SC\_BAD\_REQUEST);  
    response.setContentType(APPLICATION\_JSON\_VALUE);  
    response.setCharacterEncoding("utf-8");  
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD\_REQUEST, "JWT Token이 존재하지 않습니다.");  
    new ObjectMapper().writeValue(response.getWriter(), errorResponse);  
}  

// 만료된 토큰인 경우 error 처리  
public void tokenExpired(HttpServletResponse response) throws IOException {  
    log.info("TokenProvider : JWT Token이 만료되었습니다.");  
    response.setStatus(SC\_UNAUTHORIZED);  
    response.setContentType(APPLICATION\_JSON\_VALUE);  
    response.setCharacterEncoding("utf-8");  
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다.");  
    new ObjectMapper().writeValue(response.getWriter(), errorResponse);  
}

 

 

[참고자료]