🔍 검색 키워드: JWT 에러, jwt expired 해결, invalid signature jwt, TokenExpiredError, JWT 트러블슈팅, jwt 토큰 에러, Spring JWT 에러, Node.js jwt 에러, jwt 인증 오류
JWT를 처음 붙이든, 실무에서 수년째 쓰든 어느 날 갑자기 TokenExpiredError 또는 invalid signature가 튀어나온다. 원인은 뻔한데 처음 보면 당황스럽다. 이 글에서 JWT 에러의 종류별 원인과 해결법을 레벨별로 정리한다.
JWT 에러 종류 한눈에 보기
| 에러 메시지 | 원인 | 긴급도 |
|---|---|---|
TokenExpiredError: jwt expired | 토큰 유효기간 만료 | 보통 (정상 흐름) |
JsonWebTokenError: invalid signature | 키 불일치 또는 토큰 변조 | 높음 |
JsonWebTokenError: jwt malformed | 토큰 형식 자체가 잘못됨 | 높음 |
NotBeforeError: jwt not active | nbf 클레임 이전에 사용 | 낮음 |
JsonWebTokenError: invalid algorithm | 알고리즘 불일치 | 높음 |
LEVEL 1 초보자: 기본 개념부터
JWT는 세 파트로 구성된다: Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (알고리즘, 타입)
.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjox... ← Payload (클레임: sub, exp, iat 등)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_... ← Signature (서명)
Signature는 HMAC-SHA256(base64(header) + "." + base64(payload), secretKey)로 만든다.
토큰이 변조되면 서명 검증에서 실패한다. 이것이 JWT 보안의 핵심.
가장 흔한 실수 — 토큰을 Authorization 헤더에 제대로 안 보냄
# 잘못된 예
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 올바른 예
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
LEVEL 2 중급자: 에러별 원인과 해결
TokenExpiredError: jwt expired
토큰 발급 시 설정한 exp(만료시간)가 지났을 때 발생한다. 정상적인 흐름이다.
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ code: 'TOKEN_EXPIRED' });
}
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ code: 'INVALID_TOKEN' });
}
}
Java (Spring Boot + jjwt)
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new CustomException(ErrorCode.TOKEN_EXPIRED);
} catch (SignatureException e) {
throw new CustomException(ErrorCode.INVALID_SIGNATURE);
}
}
Python (PyJWT)
import jwt
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="토큰이 만료되었습니다.")
except jwt.InvalidSignatureError:
raise HTTPException(status_code=401, detail="유효하지 않은 토큰입니다.")
JsonWebTokenError: invalid signature
가장 많은 삽질을 유발하는 에러다. 원인은 크게 세 가지.
원인 1 — 서버 재시작 후 환경변수가 바뀜
echo $JWT_SECRET # 양쪽 서버에서 값이 같은지 확인
원인 2 — HS256 vs RS256 알고리즘 불일치
// 발급 시 RS256 사용
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 검증 시 반드시 publicKey + 알고리즘 명시
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // ✅
HS256: 대칭키 (같은 secret으로 발급·검증)
RS256: 비대칭키 (privateKey로 발급, publicKey로 검증)
원인 3 — URL 전달 시 토큰이 잘림
const url = `/verify?token=${encodeURIComponent(token)}`;
const token = decodeURIComponent(req.query.token);
JsonWebTokenError: jwt malformed
const authHeader = req.headers.authorization; // "Bearer eyJ..."
const token = authHeader.split(' ')[1]; // Bearer 제거 후 verify
jwt.verify(token, secret);
LEVEL 3 고급자: 실무 패턴
Access Token + Refresh Token 구조
function issueTokens(userId) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
Spring Security + JWT 필터 체인
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
String token = bearer.substring(7);
try {
Claims claims = jwtProvider.parseToken(token);
SecurityContextHolder.getContext().setAuthentication(
jwtProvider.getAuthentication(claims));
} catch (ExpiredJwtException e) {
response.setStatus(401);
response.getWriter().write("{"code":"TOKEN_EXPIRED"}");
return;
}
}
filterChain.doFilter(request, response);
}
}
상황별 체크리스트
| 상황 | 확인 항목 |
|---|---|
invalid signature | 양쪽 환경의 JWT_SECRET 값이 동일한가? |
invalid signature | 알고리즘이 HS256/RS256으로 일치하는가? |
invalid signature | RS256이라면 검증 측에 publicKey를 쓰고 있는가? |
jwt malformed | Bearer prefix를 제거하고 verify 하는가? |
jwt malformed | URL 전달 시 encodeURIComponent 처리 했는가? |
| 토큰이 금방 만료됨 | expiresIn 단위: '1h'(1시간) vs 3600(초) |
| 서버 재시작 후 전체 만료 | 환경변수 기반 secret인가? |
디버깅 도구
jwt.io — 토큰을 붙여넣으면 header, payload, signature를 즉시 디코딩해준다.
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig" \
| cut -d'.' -f2 | base64 --decode 2>/dev/null
🗂 정리
JWT 에러의 90%는 세 가지다: 키 불일치, 알고리즘 불일치, 토큰 만료.
JWT 에러의 90%는 세 가지다: 키 불일치, 알고리즘 불일치, 토큰 만료.
invalid signature가 나오면 키와 알고리즘을 먼저 의심하고, 환경변수가 양쪽에서 같은지 확인하는 것이 제일 빠른 경로다.TokenExpiredError는 에러가 아니라 정상 흐름이니 클라이언트 리프레시 로직을 갖춰두면 된다.