레이블이 JWT인 게시물을 표시합니다. 모든 게시물 표시
레이블이 JWT인 게시물을 표시합니다. 모든 게시물 표시

금요일

JWT 에러 완전 정복: TokenExpiredError, invalid signature 원인과 해결법

🔍 검색 키워드: 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 activenbf 클레임 이전에 사용낮음
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 signatureRS256이라면 검증 측에 publicKey를 쓰고 있는가?
jwt malformedBearer prefix를 제거하고 verify 하는가?
jwt malformedURL 전달 시 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%는 세 가지다: 키 불일치, 알고리즘 불일치, 토큰 만료.
invalid signature가 나오면 키와 알고리즘을 먼저 의심하고, 환경변수가 양쪽에서 같은지 확인하는 것이 제일 빠른 경로다.
TokenExpiredError는 에러가 아니라 정상 흐름이니 클라이언트 리프레시 로직을 갖춰두면 된다.