JWT 토큰 완벽 이해 — 구조, 검증, 보안 취약점과 올바른 사용법

"API 응답에 eyJhbGci...로 시작하는 긴 문자열이 있는데 그게 뭐죠?"
그게 바로 JWT(JSON Web Token)입니다. 2015년 RFC 7519로 표준화된 이후 현대 웹·모바일 앱의 주된 인증 방식이 됐습니다. 그런데 JWT는 잘못 쓰면 치명적인 보안 구멍이 되기도 합니다. 이번 글에서는 JWT의 구조, 올바른 사용법, 흔한 실수를 정리합니다.
JWT 기본 구조 — 점 두 개로 3부분
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuqwgOuvvOynkCIsImlhdCI6MTUxNjIzOTAyMn0 .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ┌─────────────┬─────────────┬──────────┐ │ HEADER │ PAYLOAD │ SIGNATURE│ └─────────────┴─────────────┴──────────┘
① Header — 알고리즘·타입 지정
{
"alg": "HS256", // 서명 알고리즘 (HS256, RS256, ES256 등)
"typ": "JWT"
}
② Payload — 클레임(Claim)
{
"sub": "1234567890", // subject (사용자 ID)
"name": "김철수",
"role": "admin",
"iat": 1516239022, // issued at (발급 시간)
"exp": 1516242622 // expiration (만료 시간)
}
- Registered Claims — iss(발급자), sub(주체), aud(대상), exp(만료), nbf(시작), iat(발급), jti(토큰ID)
- Public Claims — IANA 등록된 공용 클레임
- Private Claims — 커스텀 (서비스별 정의)
③ Signature — 위변조 방지
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECRET_KEY )
서버만 알고 있는 SECRET_KEY로 서명. 클라이언트가 Payload를 위조하려 해도 Signature가 맞지 않아 검증 실패.
⚠️ 가장 중요한 사실 — Payload는 "암호화"가 아니다
JWT는 Base64 인코딩이지 암호화가 아닙니다. Payload는 누구나 jwt.io 같은 도구로 디코딩해 볼 수 있습니다. 비밀번호·신용카드·민감정보는 절대 넣지 마세요. 필요하면 JWE(JSON Web Encryption) 사용.
서명 알고리즘 3가지
| 알고리즘 | 방식 | 용도 |
|---|---|---|
| HS256 | HMAC-SHA256 (대칭키) | 단일 서버, 간단 |
| RS256 | RSA-SHA256 (비대칭) | MSA·OAuth·Auth0 |
| ES256 | ECDSA-SHA256 | 모바일·성능 중요 |
대칭(HS256): 서버에서 발급·검증 모두 같은 키. 단순.
비대칭(RS256·ES256): 비밀키로 발급, 공개키로 검증. 여러 서버·3rd party 검증 가능.
세션 vs JWT — 언제 무엇을?
| 항목 | 세션 | JWT |
|---|---|---|
| 저장 위치 | 서버 | 클라이언트 |
| 확장성 | 스케일 어려움 | Stateless, 수평 확장 |
| 즉시 무효화 | 쉬움 | 어려움 (블랙리스트 필요) |
| 모바일·SPA | CORS 복잡 | 유리 |
| 데이터 포함 | ID만 | Payload 포함 |
Access + Refresh 토큰 전략
단일 JWT만 쓰면 보안과 UX가 충돌합니다. 현대 아키텍처는 Access Token(짧음) + Refresh Token(길음) 조합을 씁니다.
- Access Token — 15분~1시간, API 호출에 사용
- Refresh Token — 2주~1개월, Access 재발급에 사용
- Refresh는 httpOnly 쿠키 — JavaScript에서 접근 불가, XSS 방어
- Refresh 1회용(Rotation) — 사용 시 새 Refresh 발급, 기존 무효화
JWT 자주 발생하는 보안 취약점 10가지
- alg=none 공격 — 헤더
{"alg":"none"}으로 변경 시 서명 검증 생략. 반드시 서버가 허용 알고리즘 화이트리스트 적용 - 약한 SECRET_KEY — "secret" 같은 평문. 최소 32바이트 랜덤
- HS256 ↔ RS256 혼용 — 공개키를 HMAC 키로 오용
- Token in URL — 쿼리스트링 노출 시 브라우저 히스토리·로그에 남음
- localStorage 저장 — XSS 취약. httpOnly 쿠키 권장
- 긴 만료 시간 — Access 토큰 1일 이상 금물
- 로그아웃 미구현 — JWT는 stateless라 "무효화" 어려움. 블랙리스트 또는 짧은 TTL 필요
- 민감정보 Payload — 비밀번호·카드번호 금지
- 서명 검증 누락 — 디코딩만 하고 검증 안 하는 버그
- CSRF 방어 미비 — 쿠키 사용 시 SameSite·CSRF 토큰 필수
코드 예제 (Node.js)
// 발급
const jwt = require('jsonwebtoken')
const token = jwt.sign(
{ sub: userId, role: 'admin' },
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
)
// 검증
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // 화이트리스트 필수!
})
console.log(payload.sub)
} catch (e) {
// TokenExpiredError, JsonWebTokenError 등
}
JWT 디코더 무료
개발 중 JWT 내용을 확인하고 싶을 때 이용하세요. 온라인 디코더는 서버 전송 없이 클라이언트에서만 처리되는 것을 확인하고 사용해야 합니다.
참고
• RFC 7519 — JSON Web Token (JWT).
• OWASP JWT Security Cheat Sheet.
• jwt.io Debugger 및 Auth0 문서.