jwt

jwt 토큰 정보 읽어오기 & 예외처리

원코드 2024. 9. 4. 15:34

1. 토큰 분석(parse)

토큰을 생성할 때 사용한 secret Key를 전달하여 JwtParser를 생성하고, JwtParser의 parser() 함수를 이용하여 토큰을 분석한다.

private Jws<Claims> parseToken(final String token) {
		return Jwts.parser()
				//.setSigningKey(secretKey) -> deprecation
				.verifyWith(secretKey)
				.build()
				//.parseClaimsJws(token)// -> deprecation
				.parse(token)
				.accept(Jws.CLAIMS);
				
	}

verifyWith(Key key) : jwt 생성 시 사용한 시크릿 키

build() : JwtParser 리턴

parse(CharSequence jwt) : build()를 통해 생성한 JwtParser에 분석하고자 하는 jwt 토큰을 인자로 전달하여 결과로 jwt, jws, jwe 리턴

accept(JwtVisitor<Jws<Claims>> visitor) : parse(jwt)의 결과가 JWT, JWS 또는 JWE인지, 페이로드가 인스턴스 확인이 포함된 Claimsor byte[] 배열인지 확인하는 함수. if문 대신 유용하게 사용할 수 있음.

 

accept() 대신 if문을 사용할 경우(권장되지 않음)

// NOT RECOMMENDED:
 Jwt<?,?> jwt = parser.parse(input);
 if (jwt instanceof Jwe<?>) {
     Jwe<?> jwe = (Jwe<?>)jwt;
     if (jwe.getPayload() instanceof Claims) {
         Jwe<Claims> claimsJwe = (Jwe<Claims>)jwe;
         // do something with claimsJwe
     }
 }

 

 

2. 예외처리(커스텀)

parse(token) 호출 시 아래와 같은 exception이 발생할 수 있으므로 예외처리를 해주자.

Throws:
MalformedJwtException - if the specified JWT was incorrectly constructed (and therefore invalid).Invalid JWTs should not be trusted and should be discarded.
SignatureException - if a JWS signature was discovered, but could not be verified. JWTs that failsignature validation should not be trusted and should be discarded.
SecurityException - if the specified JWT string is a JWE and decryption fails
ExpiredJwtException - if the specified JWT is a Claims JWT and the Claims has an expiration timebefore the time this method is invoked.
IllegalArgumentException - if the specified string is null or empty or only whitespace.

 

예외처리 함수

public void validateTokens(final String token) {
		try {
			parseToken(token);
		} catch (final ExpiredJwtException e) {
            throw new ExpiredPeriodJwtException(ExceptionCode.EXPIRED_PERIOD_ACCESS_TOKEN); //커스텀 exception
        } catch (final MalformedJwtException | IllegalArgumentException e) {
            throw new InvalidJwtException(ExceptionCode.EXPIRED_PERIOD_ACCESS_TOKEN); //커스텀 exception
        }
	}

 

예외처리 클래스

ExpiredPeriodJwtException.class

import lombok.Getter;

@Getter
public class ExpiredPeriodJwtException extends AuthException{

	public ExpiredPeriodJwtException(ExceptionCode exceptionCode) {
		super(exceptionCode);
	}
}

 

InvalidJwtException.class

import lombok.Getter;

@Getter
public class InvalidJwtException extends AuthException{

	public InvalidJwtException(final ExceptionCode exceptionCode) {
		super(exceptionCode);
	}
}

 

AuthException.class

import lombok.Getter;

@Getter
public class AuthException extends RuntimeException{
	
	private final int code;
	private final String message;
	
	public AuthException(final ExceptionCode code) {
		this.code = code.getCode();
		this.message = code.getMessage();
	}
	
}

 

그리고 예외처리 결과를 json 형태로 클라이언트에 전달해 줄 핸들러 클래스

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(AuthException.class)
	public ResponseEntity<ExceptionResponse> handleAuthException(AuthException e) {
		log.warn(e.getMessage(), e);
		return ResponseEntity.badRequest().body(new ExceptionResponse(e.getCode(), e.getMessage()));
	}
}

 

3. 테스트

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.Random;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import com.tlog.backend.global.exception.ExpiredPeriodJwtException;
import com.tlog.backend.global.exception.InvalidJwtException;

@SpringBootTest
@TestPropertySource(locations = "classpath:application.yml")
public class JwtProviderTest {

	@Value("${jwt.secret}")
	String secretKey;
	@Value("${jwt.refresh_expiration_time}")
	Long refreshTime;
	@Value("${jwt.access_expiration_time}")
	Long accessTime;
	
	@Test
	@DisplayName("토큰이 만료되었을 때 ExpiredPeriodJwtException 발생한다.")
	void validateTokens_test1() {
		//given
		JwtProvider provider = new JwtProvider(secretKey, refreshTime, accessTime);
		Long memberId = new Random().nextLong();
		String token = provider.createToken(memberId, 0L);
		
		//when, then
		assertThrows(ExpiredPeriodJwtException.class, ()->{provider.validateTokens(token);});
		
	}
	
	@Test
	@DisplayName("토큰 값이 잘못된 경우 InvalidJwtException 발생한다.")
	void validateTokens_test2() {
		//given
		JwtProvider provider = new JwtProvider(secretKey, refreshTime, accessTime);
		String token = null;
		
		//when, then
		assertThrows(InvalidJwtException.class, ()->{provider.validateTokens(token);});
	}
	
}