[Auth] JWT: 구조, 서명, 만료, refresh token
정의
JWT (JSON Web Token) (RFC 7519) 은 base64url 인코딩된 JSON + 서명 형태의 self-contained token. 서버가 세션 저장 없이 클레임 검증 가능.
JWS vs JWE: 두 가지 형태
| 형태 | 풀네임 | 기능 | 사용 |
|---|---|---|---|
| JWS | JSON Web Signature | 서명 (무결성 보장) | 대부분의 JWT (인증, 인가) |
| JWE | JSON Web Encryption | 암호화 (기밀성 보장) | 민감 정보 페이로드 필요 시 |
JWS: header.payload.signature (페이로드 가시)
JWE: header.key.iv.ciphertext.tag (페이로드 암호화)
일반적으로 “JWT” 는 JWS 를 의미. 페이로드는 암호화 아님 (base64 인코딩만).
구조: 3 부분
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJrb2EiLCJleHAiOjE3MjAxMjM0NTZ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
↑ header ↑ payload ↑ signature
| 부분 | 형식 |
|---|---|
| Header | { "alg": "HS256", "typ": "JWT" } |
| Payload | { "sub": "42", "name": "koa", "exp": 1720123456 } |
| Signature | HMAC-SHA256(base64(header) + ”.” + base64(payload), secret) |
표준 클레임
| 클레임 | 의미 |
|---|---|
iss | issuer |
sub | subject (user ID) |
aud | audience |
exp | 만료 시각 (Unix epoch) |
nbf | not before |
iat | issued at |
jti | JWT ID (unique, replay 방지) |
서명 알고리즘: HS256 vs RS256
| Alg | 종류 | 키 | 비고 |
|---|---|---|---|
HS256/384/512 | HMAC (대칭) | 공유 secret | 양측이 같은 키. 내부 서비스 |
RS256/384/512 | RSA (비대칭) | private (sign) / public (verify) | 권장. 공개키 배포 가능 |
ES256/384/512 | ECDSA | private / public | 짧고 빠름 |
EdDSA (Ed25519) | EdDSA | private / public | 가장 빠름, 안전 |
none | 서명 없음 | - | 절대 사용 금지 |
sequenceDiagram
participant Auth as AuthServer
participant API as APIServer
participant Client as Client
Note over Auth,API: RS256 키 배포 (1회)
Auth->>API: 공개키 (JWKS endpoint or 환경변수)
Note over Client,API: 인증 흐름
Client->>Auth: POST /login
Auth->>Auth: JWT 생성 (private key 로 서명)
Auth-->>Client: access_token (JWT)
Client->>API: GET /resource (Authorization: Bearer JWT)
API->>API: 공개키로 서명 검증 (Auth 서버 접속 없음)
API-->>Client: 200 OK
IMPORTANT
RS256: Auth Server 만 private key 보유. API Server 는 public key 로 검증. public key 유출돼도 토큰 위조 불가. HS256 은 secret 공유 필요 → 서비스 수 늘어날수록 위험.
흐름
sequenceDiagram
autonumber
participant C as Client
participant Auth as AuthServer
participant API as APIServer
C->>Auth: POST /login (id, password)
Auth->>Auth: 검증 + JWT 생성
Auth-->>C: { access_token, refresh_token }
C->>API: GET /me (Authorization: Bearer JWT)
API->>API: JWT 검증 (서명 + exp)
API-->>C: 200 OK
Note over C: 만료 임박
C->>Auth: POST /refresh (refresh_token)
Auth-->>C: 새 access_token
Refresh Token Rotation 패턴
sequenceDiagram
participant C as Client
participant Auth as AuthServer
C->>Auth: POST /refresh (RT=abc)
Auth->>Auth: RT 검증, 새 AT + 새 RT 발급
Auth->>Auth: 구 RT(abc) 무효화
Auth-->>C: { access_token, refresh_token: xyz }
Note over C,Auth: 탈취된 RT 로 재시도
C->>Auth: POST /refresh (RT=abc 재사용 시도)
Auth->>Auth: abc 무효화 확인
Auth-->>C: 401 Unauthorized
Auth->>Auth: 계정 전체 세션 강제 종료 (재사용 감지)
RT rotation: 매 refresh 마다 새 RT 발급 + 구 RT 즉시 무효화. 재사용 시 탈취 감지 → 전체 세션 폐기.
Access Token vs Refresh Token
| 항목 | Access Token | Refresh Token |
|---|---|---|
| 수명 | 짧음 (5분 ~ 1시간) | 길음 (7일 ~ 30일) |
| 저장 | 메모리 (브라우저 SPA) | HttpOnly cookie 또는 안전한 저장 |
| 매 요청 전송 | 예 | 갱신 시만 |
| 탈취 영향 | 짧은 시간 피해 | 큰 피해, 반드시 HttpOnly |
| Revoke | 만료까지 어려움 | DB 에 저장 + revoke 가능 |
IMPORTANT
Access token 을 localStorage 에 저장 하면 XSS 시 즉시 탈취. 메모리 + 새로고침 시 refresh 패턴이 정통.
alg=none 취약점
# 취약한 라이브러리 예시 (개념도)
import jwt
# 공격자가 만든 페이로드
malicious_header = {"alg": "none", "typ": "JWT"}
malicious_payload = {"sub": "1", "role": "admin", "exp": 9999999999}
# base64 인코딩 후 서명 없이 전송
token = base64(malicious_header) + "." + base64(malicious_payload) + "."
# → 취약 라이브러리는 서명 검증 없이 통과
방어:
# 안전한 사용법: 허용 alg 화이트리스트
decoded = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"]) # alg 명시
# algorithms=["RS256"] 에 없는 알고리즘은 거절
WARNING
alg: none 을 받는 라이브러리는 치명적 취약점. 항상 허용 alg 화이트리스트. 사용 라이브러리의 CVE 이력 확인.
Revoke 전략
flowchart TD
Q{revoke 필요}
Q --> A["짧은 exp + refresh<br/>대부분의 경우"]
Q --> B["Blacklist (Redis)<br/>jti 등록"]
Q --> C["Versioning<br/>user.token_version 비교"]
A --> Best[추천]
B --> Heavy[캐시 부담]
C --> Sync[DB 한 번 조회 필요]
Redis Blacklist 구현:
// 로그아웃 시 jti 를 blacklist 에 등록
async function logout(jti: string, exp: number) {
const ttl = exp - Math.floor(Date.now() / 1000);
await redis.setex(`blacklist:${jti}`, ttl, "1");
}
// 검증 미들웨어
async function verifyToken(token: string) {
const payload = jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] });
const isBlacklisted = await redis.exists(`blacklist:${payload.jti}`);
if (isBlacklisted) throw new Error("Token revoked");
return payload;
}
XSS / CSRF 와의 관계
flowchart TD
Store{저장 위치}
Store --> A[localStorage] -->|XSS 시 탈취| Bad1[X]
Store --> B["메모리 + HttpOnly cookie refresh"] -->|XSS 영향 적음| Good[O]
Store --> C["HttpOnly cookie + SameSite=Strict"] -->|CSRF 안전 + XSS 안전| Best[권장]
IMPORTANT
Cookie 기반 이면 CSRF 방어 필요 (SameSite=Strict 또는 CSRF token). Bearer header 면 XSS 방어 가 중요.
Stateless vs Stateful
| Session (stateful) | JWT (stateless) | |
|---|---|---|
| 서버 저장소 | 필수 | 불필요 |
| Revoke | 즉시 | 만료까지 어려움 |
| 스케일 | sticky session 필요 | 수평 확장 자유 |
| 페이로드 | session ID 만 (작음) | 전체 클레임 (큼) |
| 갱신 | 서버 갱신 | 새 JWT 발급 |
CAUTION
“JWT 가 항상 더 좋다” 는 미신. 작은 monolith 에서는 session 이 더 단순 + 즉시 revoke 가능. JWT 는 분산 + 마이크로서비스 + 모바일 SDK 에서 진가.
흔한 함정
WARNING
alg: none허용 라이브러리 = 옛 라이브러리 일부. 즉시 업데이트.- HS256 vs RS256 혼동 = 공개키를 secret 으로 오해해 서명 받으면 모든 토큰 위조 가능 (CVE 다수).
- 만료 너무 김 = 탈취 시 피해 큼. 15분 ~ 1시간 권장.
- secret 이 짧음 =
secret같이 무차별 공격 가능한 키. 32+ 바이트 랜덤. - 민감 정보 페이로드 = JWT 는 암호화 아님 (base64 인코딩). password / 카드 번호 절대 금지.
- Refresh token localStorage 저장 = XSS 취약. 반드시 HttpOnly cookie.
- RT rotation 미적용 = 탈취된 RT 무한 재사용 가능.
JWT 크기와 성능 고려사항
JWT 는 매 요청 헤더에 포함. 크기가 커지면 네트워크 오버헤드:
| 클레임 수 | 대략 크기 (HS256) |
|---|---|
| 최소 (sub, exp, iat) | ~150 bytes |
| 일반 (10개 클레임) | ~300-400 bytes |
| 과도 (role, permission 배열 포함) | 1KB+ |
클레임 최소화: role 이름 대신 role ID, permission 배열 대신 scope string 으로 압축. 큰 권한 데이터는 DB 조회로.
JWE (암호화 변형)
JWT 가 서명만 이면 내용 노출. JWE 는 암호화 까지. 그러나 느리고 복잡. 대부분의 경우 민감 정보를 페이로드에 안 넣고 JWS (서명만) 만으로 충분.
관련 위키
이 글의 용어 (5개)
- [Auth] CSRF: 위조 요청 공격과 방어auth-security
- 정의 CSRF (Cross-Site Request Forgery) 는 사용자가 로그인된 상태 에서 공격자가 만든 페이지 가 피해자 브라우저로 위조 요청 을 보내는 공격. 쿠키 기…
- [Auth] OAuth 2.0 / 2.1: Authorization Code + PKCEauth-security
- 정의 OAuth 2.0 (RFC 6749, 2012) 은 제3자 앱이 사용자 동의로 자원에 접근 하게 하는 권한 위임 프레임워크. 인증 (authentication) 이 아니라 …
- [Auth] OpenID Connect (OIDC): OAuth 위의 인증 레이어auth-security
- 정의 OpenID Connect (OIDC) 는 OAuth 2.0 위에 얹은 인증 (authentication) 표준. OAuth 가 권한 위임 (authorization), O…
- [Auth] Session Cookie: HttpOnly, SameSite, Secure flagauth-security
- 정의 Session Cookie 는 서버가 발급한 임의 ID 를 쿠키에 보관. 매 요청 자동 첨부. 서버 측 세션 store (Redis, DB) 와 짝. [!IMPORTANT]…
- [Network] CORS: Same-Origin Policy, preflight, credentialsnetwork
- 정의 CORS (Cross-Origin Resource Sharing) 는 Same-Origin Policy (SOP) 의 완화 메커니즘. 브라우저가 origin 이 다른 리소스…
이 개념을 다룬 위키 페이지 (10)
- wiki[API Design] OpenAPI / Swagger: 스펙, 코드 생성, contract testing
- wiki[Auth] CSRF: 위조 요청 공격과 방어
- wiki[Auth] OAuth 2.0 / 2.1: Authorization Code + PKCE
- wiki[Auth] OpenID Connect (OIDC): OAuth 위의 인증 레이어
- wiki[Auth] SAML 2.0: XML 기반 기업 SSO
- wiki[Auth] Session Cookie: HttpOnly, SameSite, Secure flag
- wikiStateless
- wiki[Pattern] API Gateway: BFF, 라우팅, auth, rate limit
- wiki[DRF] Authentication: Session, Token, JWT, OAuth2
- wiki[Network] CORS: Same-Origin Policy, preflight, credentials
💬 댓글