💬 들어가기앞서 . . .
JWT와 OAuth2 기반 인증 시스템을 직접 구현하면서, 생각보다 많은 예외 상황과 마주하게 되었습니다.
그중에서도 accessToken이 만료된 이후의 처리 흐름에서 예상치 못한 에러가 발생하며, 인증 로직 전반을 다시 점검할 필요가 있었습니다.
이 글은 refreshToken을 통한 accessToken 재발급 과정 중 발생한 에러와 그 원인, 해결 방법을 정리한 포스팅입니다.
저처럼 인증 흐름을 처음 설계하거나 JWT 관련 에러로 어려움을 겪고 있는 분들께 도움이 되기를 바랍니다!
🛠️ [트러블슈팅] io.jsonwebtoken.ExpiredJwtException: JWT expired 564427
문제 상황 (Issue)
프로젝트를 진행하며, 로그인 시 발급되는 accessToken은 Cookie에 저장하고, refreshToken은 Redis에서 관리하도록 구현하였습니다. accessToken이 만료되었을 경우, Redis에 저장된 refreshToken을 이용해 사용자의 정보를 가져온 뒤에 accessToken의 사용자 정보와 비교한 뒤 일치하면 새로운 accessToken을 발급받는 방식입니다.
그런데 refreshToken으로 사용자의 정보를 가져오는 단계에서 아래와 같은 에러가 발생하는 것을 확인하였습니다.
io.jsonwebtoken.ExpiredJwtException: JWT expired 564427 milliseconds ago at 2025-03-17T07:44:18.000Z. Current time: 2025-03-17T07:53:42.427Z. Allowed clock skew: 0 milliseconds.
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:682) ~[jjwt-impl-0.12.3.jar:0.12.3]
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:362) ~[jjwt-impl-0.12.3.jar:0.12.3]
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94) ~[jjwt-impl-0.12.3.jar:0.12.3]
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36) ~[jjwt-impl-0.12.3.jar:0.12.3]
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29) ~[jjwt-impl-0.12.3.jar:0.12.3]
at io.jsonwebtoken.impl.DefaultJwtParser.parseSignedClaims(DefaultJwtParser.java:821) ~[jjwt-impl-0.12.3.jar:0.12.3]
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:797) ~[jjwt-impl-0.12.3.jar:0.12.3]
at com.project.growfit.global.auto.jwt.JwtProvider.isExpired(JwtProvider.java:98) ~[main/:na]
at com.project.growfit.global.auto.jwt.filter.JwtCookieAuthenticationFilter.doFilter(JwtCookieAuthenticationFilter.java:40) ~[main/:na]
이로 인해 정상적인 토큰 갱신이 이루어지지 않았고, 인증 흐름 전체에 영향을 주게 되었습니다.
원인 분석 (Cause)
✅ 원인 1: JJWT 라이브러리의 동작 특성
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
저는 위와같은 라이브러리를 사용하고있습니다.
이 라이브러리는 기한이 만료된 토큰(Expired Token) 에서 subject(혹은 claim 등)를 추출하려고 하면, 자동으로 ExpiredJwtException을 발생시키도록 설계되어 있습니다.
즉, 만료된 토큰에 대해 claim 정보만 확인하려 해도, 예외를 처리하지 않으면 흐름이 끊겨버리는 상황이 발생합니다.
해결 방법 (Solution)
그러면 기한이 만료된 토큰에서 claim을 추출하려면 어떻게 처리해야할까?
예외가 강제적으로 발생하는것이니, try-catch를 이용해 예외를 잡고 그 예외의 claim을 이용해 정보를 추출하면 됩니다.
jwtUtil.java
//기존의 코드
public String getUserId(String token) {
try {
String userId = Jwts.parser().setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().get("user_id", String.class);
log.debug("[getUserId] 토큰에서 사용자 ID 추출 성공: {}", userId);
return userId;
}
}
//수정 코드
public String getUserId(String token) {
try {
String userId = Jwts.parser().setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().get("user_id", String.class);
log.debug("[getUserId] 토큰에서 사용자 ID 추출 성공: {}", userId);
return userId;
} catch (ExpiredJwtException e) {
String userId = e.getClaims().get("user_id", String.class);
log.debug("[getUserId] 토큰 만료됨. 만료 토큰에서 사용자 ID 추출: {}", userId);
return userId;
}
}
코드를 수정한 뒤, accessToken 만료 시간을 1분으로 둔 뒤에 테스트해보았습니다.
AccessToken이 성공적으로 재발급 되는 것을 확인할 수 있었습니다.
참고 자료 (Reference)
is there a way to parse claims from an expired JWT token?
If we try to parse an expired JWT, results in expired exception. Is there a way to read claims even the JWT was expired. Below is used to parse JWT in java: Jwts.parser().setSigningKey(secret.ge...
stackoverflow.com
✨ 게시글을 마무리하며
만료된 JWT에서 단순히 claim만 추출하려다 예외가 발생한 상황은 예상치 못한 트러블이었지만, 그만큼 많이 배울 수 있었던 경험이었습니다. 저와 비슷한 상황에 놓인 분들께 작게나마 도움이 되었으면 좋겠습니다 :)
글 읽어주셔서 감사합니다.
'Spring > Error' 카테고리의 다른 글
[ 트러블슈팅 ]데이터 정합성 이슈: 커밋 시점으로 인해 발생한 동시성 문제 (0) | 2025.09.30 |
---|---|
[ 트러블슈팅 ] 낙관적 락 동시성 처리 에러 (0) | 2025.09.19 |
[트러블 슈팅] Authentication Principal is not of type CustomUserDetails. Actual type: java.long.String (0) | 2025.03.01 |
[트러블슈팅] application.yml에서 oauth2 설정이 적용되지 않는 이슈 (0) | 2025.02.12 |
[Jwt]Error creating bean with name 'springSecurityFilterChain' defined in class path resource (0) | 2024.08.21 |