JWT 토큰 완벽 해설: 구조, 보안, 그리고 흔한 실수들
JSON Web Token의 작동 방식, 내부 구조, 그리고 프로덕션 환경에서 개발자들이 빠지기 쉬운 보안 함정을 알아봅니다.
JSON Web Token(JWT)은 어디에나 있습니다 — SPA, 모바일 앱, 전 세계 마이크로서비스 아키텍처의 인증을 담당하고 있죠. 그러나 동시에 웹 개발에서 가장 많이 오해받는 보안 기본 요소 중 하나이기도 합니다. 잘못 사용하면 인증 우회, 권한 상승, 또는 계정 완전 탈취로 이어질 수 있습니다.
JWT란 무엇인가?
JWT는 클레임(claims) — 사용자나 세션에 대한 정보 — 을 인코딩한 간결하고 URL-safe한 문자열입니다. 다음과 같이 생겼습니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
점(.)으로 구분된 세 개의 Base64URL 인코딩 세그먼트로 구성됩니다:
- Header — 알고리즘과 토큰 타입
- Payload — 클레임 (사용자 데이터)
- Signature — 무결성을 증명하는 암호화 서명
우리의 JWT Decoder에 JWT를 붙여넣으면 외부 서버를 거치지 않고 세 부분을 즉시 확인할 수 있습니다.
JWT 구조 분석
Header
{
"alg": "HS256",
"typ": "JWT"
}
alg는 서명 알고리즘을 지정합니다. 주요 값:
HS256— HMAC with SHA-256 (대칭키, 단일 시크릿)RS256— RSA with SHA-256 (비대칭키, 공개/개인 키)ES256— ECDSA with SHA-256 (비대칭키, 더 짧은 키)
Payload
{
"sub": "user_abc123",
"email": "alice@example.com",
"role": "admin",
"iat": 1711670400,
"exp": 1711756800
}
표준 등록 클레임:
| 클레임 | 의미 |
|---|---|
sub |
Subject (토큰의 대상) |
iss |
Issuer (토큰 발급자) |
aud |
Audience (토큰 수신 대상) |
exp |
Expiration time (Unix 타임스탬프) |
iat |
Issued at (Unix 타임스탬프) |
nbf |
Not before (이 시간 이전에는 수락 불가) |
Signature
HS256의 경우:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
서명은 토큰이 변조되지 않았음을 증명합니다. Payload는 암호화되지 않으며 — 단순히 인코딩된 것입니다. 누구든 읽을 수 있습니다.
비밀번호나 신용카드 번호 같은 민감한 데이터를 JWT payload에 저장하지 마세요.
HS256 vs. RS256: 무엇을 선택해야 할까?
HS256은 하나의 공유 시크릿을 사용합니다. 토큰을 검증해야 하는 모든 서비스가 동일한 시크릿을 보유해야 합니다. 단일 서버 환경에서는 간단하지만, 멀티 서비스 아키텍처에서는 위험합니다 — 서비스 하나가 침해되면 공격자가 토큰을 위조할 수 있습니다.
RS256은 비대칭 키를 사용합니다. 인증 서버는 개인 키로 서명하고, 다른 모든 서비스는 공개 키로 검증합니다. 소비자 서비스가 침해되더라도 공격자가 토큰을 위조할 수 없습니다. 여러 서비스가 있는 시스템에서는 RS256을 권장합니다.
alg: none 취약점
가장 악명 높은 JWT 공격 중 하나입니다. 일부 구버전 라이브러리는 "alg": "none"이고 서명이 없는 토큰을 유효한 것으로 처리했습니다. 공격자는 다음과 같이 조작할 수 있었습니다:
{ "alg": "none" }
어떤 payload든 붙여서 인증을 완전히 우회할 수 있었습니다.
해결책: JWT 라이브러리에서 허용할 알고리즘을 항상 명시적으로 지정하세요. none은 절대 허용하지 마세요.
// ❌ 위험
jwt.verify(token, secret);
// ✅ 안전 — HS256만 허용
jwt.verify(token, secret, { algorithms: ["HS256"] });
알고리즘 혼동 공격
또 다른 심각한 취약점: 라이브러리가 헤더에서 알고리즘을 자동으로 감지한다면, 공격자는 RS256을 HS256으로 변경하고 공개 키를 HMAC 시크릿으로 사용하여 토큰에 서명할 수 있습니다 (공개 키는 말 그대로 공개되어 있으므로).
해결책: 기대하는 알고리즘을 항상 하드코딩하세요. alg 헤더를 절대 신뢰하지 마세요.
토큰 저장: JWT를 어디에 보관할까?
| 저장 방식 | XSS 위험 | CSRF 위험 | 비고 |
|---|---|---|---|
localStorage |
높음 | 없음 | 페이지의 모든 JS에서 접근 가능 |
sessionStorage |
높음 | 없음 | 탭 닫으면 삭제 |
| HTTP-only 쿠키 | 없음 | 중간 | 웹 앱에 최적; SameSite=Strict 사용 |
| 메모리 (변수) | 낮음 | 없음 | 새로고침 시 소실; SPA에 적합 |
웹 애플리케이션의 경우, SameSite=Strict를 설정한 HTTP-only 쿠키가 가장 안전한 옵션입니다. 네이티브 앱의 경우 보안 저장소 API(Keychain, Keystore)가 적합합니다.
만료와 리프레시 토큰
단기 액세스 토큰(5~15분)과 장기 리프레시 토큰을 함께 사용하는 것이 표준 패턴입니다:
- 사용자 로그인 → 서버가 액세스 토큰(15분) + 리프레시 토큰(7일, DB 저장) 발급
- 클라이언트는 API 호출에 액세스 토큰 사용
- 액세스 토큰 만료 시 클라이언트가 리프레시 토큰 전송 → 새 액세스 토큰 수령
- 로그아웃 시 데이터베이스에서 리프레시 토큰 무효화
이 방식은 액세스 토큰이 탈취되더라도 피해 범위를 제한합니다.
JWT 폐기하기
JWT는 무상태(stateless)입니다 — 한번 발급되면 만료 전까지 유효합니다. 이는 트레이드오프입니다. 조기 폐기를 위한 옵션:
- 차단 목록(Blocklist) — 무효화된 JTI(JWT ID) 값을 Redis에 저장하고 매 요청마다 확인합니다.
- 짧은 만료 시간 — 5분 토큰은 피해 범위를 제한합니다.
- 리프레시 토큰 로테이션 — 이미 교체된 토큰의 재사용을 탈취 징후로 감지합니다.
JWT Decoder로 디버깅하기
인증 문제를 디버깅할 때 우리의 JWT Decoder를 활용하세요:
- 코드 없이 전체 payload 확인
- 만료 타임스탬프를 사람이 읽기 쉬운 형태로 확인
- 사용 중인 알고리즘 검증
- 예상치 못한 클레임이나 누락된 클레임 발견
모든 디코딩은 브라우저 내에서 로컬로 처리됩니다 — 토큰이 외부로 전송되지 않습니다.
JWT 보안 체크리스트
- 멀티 서비스 아키텍처에서는 RS256 또는 ES256 사용
- 라이브러리에서 허용 알고리즘을 명시적으로 지정
- 모든 요청에서
exp,iss,aud클레임 검증 - 웹 앱에서는 HTTP-only 쿠키에 토큰 저장
- 짧은 액세스 토큰 유효기간 사용 (15분 이하)
- 재사용 감지 기능이 포함된 리프레시 토큰 로테이션 구현
- payload에 민감한 데이터 절대 저장 금지
- 모든 곳에서 HTTPS 사용 — 평문 토큰은 인증이 없는 것과 같음
JWT는 강력하고 편리하지만, 구현 방식에 따라 보안 수준이 결정됩니다. 구조를 이해하고 공격 방식을 숙지한다면, 여러분의 인증 레이어는 견고해질 것입니다.