JWT
JSON Web Token
JSON 형식의 자가 포함(Self-contained) 토큰으로 Header.Payload.Signature 구조를 가집니다. 서버 세션 없이 무상태(Stateless) 인증을 가능하게 합니다.
JSON Web Token
JSON 형식의 자가 포함(Self-contained) 토큰으로 Header.Payload.Signature 구조를 가집니다. 서버 세션 없이 무상태(Stateless) 인증을 가능하게 합니다.
JWT(JSON Web Token)는 RFC 7519에 정의된 개방형 표준으로, 당사자 간 정보를 JSON 객체로 안전하게 전송하는 방법입니다. "자가 포함(Self-contained)"이란 토큰 자체에 필요한 모든 정보가 담겨 있어, 별도의 데이터베이스 조회 없이 토큰만으로 사용자를 인증할 수 있다는 의미입니다. 이로 인해 서버에 세션을 저장하지 않는 무상태(Stateless) 아키텍처가 가능합니다.
JWT는 세 부분으로 구성됩니다: Header(헤더), Payload(페이로드), Signature(서명)이며, 각 부분은 점(.)으로 구분됩니다. Header는 토큰 타입(JWT)과 서명 알고리즘(예: HS256, RS256)을 명시합니다. Payload는 클레임(Claims)을 담고 있는데, 표준 클레임(iss, sub, exp 등)과 사용자 정의 클레임이 있습니다. Signature는 Header와 Payload를 비밀 키로 서명한 값으로, 토큰 변조를 방지합니다.
HS256(HMAC-SHA256)은 대칭키 방식으로 같은 비밀 키로 서명과 검증을 합니다. 단순하지만 키를 안전하게 공유해야 하므로 단일 서비스에 적합합니다. RS256(RSA-SHA256)은 비대칭키 방식으로 개인키로 서명하고 공개키로 검증합니다. 마이크로서비스 환경에서 인증 서버만 개인키를 가지고, 다른 서비스들은 공개키로 검증할 수 있어 확장성이 좋습니다.
Access Token과 Refresh Token 패턴이 일반적입니다. Access Token은 짧은 수명(15분~1시간)을 가지며 API 요청에 사용됩니다. Refresh Token은 긴 수명(7일~30일)을 가지며 새 Access Token을 발급받는 데 사용합니다. Access Token이 탈취되어도 짧은 수명으로 피해를 최소화하고, Refresh Token Rotation을 적용하면 보안이 더 강화됩니다.
# JWT 생성 및 검증 - Python PyJWT 라이브러리
import jwt
from datetime import datetime, timedelta
from typing import Optional
import secrets
class JWTManager:
"""JWT 토큰 관리 클래스"""
def __init__(self, secret_key: str = None, algorithm: str = "HS256"):
self.secret_key = secret_key or secrets.token_hex(32)
self.algorithm = algorithm
self.access_token_expire = timedelta(minutes=30)
self.refresh_token_expire = timedelta(days=7)
def create_access_token(self, user_id: str, roles: list = None) -> str:
"""Access Token 생성"""
payload = {
"sub": user_id, # Subject (사용자 ID)
"roles": roles or [], # 사용자 역할
"type": "access",
"iat": datetime.utcnow(), # Issued At (발급 시간)
"exp": datetime.utcnow() + self.access_token_expire # 만료 시간
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def create_refresh_token(self, user_id: str) -> str:
"""Refresh Token 생성"""
payload = {
"sub": user_id,
"type": "refresh",
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + self.refresh_token_expire,
"jti": secrets.token_hex(16) # 고유 토큰 ID (revocation용)
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def verify_token(self, token: str) -> Optional[dict]:
"""토큰 검증 및 페이로드 반환"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return payload
except jwt.ExpiredSignatureError:
print("토큰이 만료되었습니다")
return None
except jwt.InvalidTokenError as e:
print(f"유효하지 않은 토큰: {e}")
return None
def refresh_access_token(self, refresh_token: str) -> Optional[str]:
"""Refresh Token으로 새 Access Token 발급"""
payload = self.verify_token(refresh_token)
if not payload:
return None
if payload.get("type") != "refresh":
print("Refresh Token이 아닙니다")
return None
# 새 Access Token 발급
return self.create_access_token(payload["sub"])
# 사용 예시
if __name__ == "__main__":
jwt_manager = JWTManager()
# 1. 로그인 시: 토큰 쌍 발급
user_id = "user_12345"
access_token = jwt_manager.create_access_token(user_id, ["user", "admin"])
refresh_token = jwt_manager.create_refresh_token(user_id)
print(f"Access Token: {access_token[:50]}...")
print(f"Refresh Token: {refresh_token[:50]}...")
# 2. API 요청 시: Access Token 검증
payload = jwt_manager.verify_token(access_token)
if payload:
print(f"인증된 사용자: {payload['sub']}, 역할: {payload['roles']}")
# 3. Access Token 만료 시: Refresh Token으로 갱신
new_access_token = jwt_manager.refresh_access_token(refresh_token)
print(f"새 Access Token: {new_access_token[:50]}...")
// JWT 생성 및 검증 - Node.js jsonwebtoken
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class JWTManager {
constructor(secretKey = null) {
this.secretKey = secretKey || crypto.randomBytes(32).toString('hex');
this.accessTokenExpire = '30m'; // 30분
this.refreshTokenExpire = '7d'; // 7일
}
// Access Token 생성
createAccessToken(userId, roles = []) {
return jwt.sign(
{
sub: userId,
roles: roles,
type: 'access'
},
this.secretKey,
{
algorithm: 'HS256',
expiresIn: this.accessTokenExpire
}
);
}
// Refresh Token 생성
createRefreshToken(userId) {
return jwt.sign(
{
sub: userId,
type: 'refresh',
jti: crypto.randomBytes(16).toString('hex')
},
this.secretKey,
{
algorithm: 'HS256',
expiresIn: this.refreshTokenExpire
}
);
}
// 토큰 검증
verifyToken(token) {
try {
return jwt.verify(token, this.secretKey);
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.log('토큰이 만료되었습니다');
} else {
console.log(`유효하지 않은 토큰: ${error.message}`);
}
return null;
}
}
// 토큰에서 페이로드 추출 (검증 없이)
decode(token) {
return jwt.decode(token, { complete: true });
}
}
// Express 미들웨어 예시
const authMiddleware = (jwtManager) => {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
const payload = jwtManager.verifyToken(token);
if (!payload) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = payload;
next();
};
};
// 사용 예시
const jwtManager = new JWTManager();
// 로그인 API
const login = (userId, roles) => {
return {
accessToken: jwtManager.createAccessToken(userId, roles),
refreshToken: jwtManager.createRefreshToken(userId),
expiresIn: 1800 // 30분 (초)
};
};
// 토큰 갱신 API
const refreshTokens = (refreshToken) => {
const payload = jwtManager.verifyToken(refreshToken);
if (!payload || payload.type !== 'refresh') {
throw new Error('Invalid refresh token');
}
return {
accessToken: jwtManager.createAccessToken(payload.sub),
// Refresh Token Rotation: 새 Refresh Token도 발급
refreshToken: jwtManager.createRefreshToken(payload.sub)
};
};
console.log(login('user123', ['admin']));
JWT 구조 분석
==============
JWT 형식: xxxxx.yyyyy.zzzzz
Header.Payload.Signature
1. Header (헤더) - Base64Url 인코딩
====================================
{
"alg": "HS256", // 서명 알고리즘
"typ": "JWT" // 토큰 타입
}
인코딩: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload (페이로드) - Base64Url 인코딩
========================================
{
// 등록된 클레임 (Registered Claims)
"iss": "https://api.example.com", // 발급자
"sub": "user_12345", // 주제 (사용자 ID)
"aud": "https://app.example.com", // 대상자
"exp": 1735689600, // 만료 시간 (Unix timestamp)
"nbf": 1735686000, // 유효 시작 시간
"iat": 1735686000, // 발급 시간
"jti": "unique-token-id-123", // JWT ID (고유 식별자)
// 사용자 정의 클레임
"roles": ["user", "admin"],
"permissions": ["read", "write"]
}
3. Signature (서명)
====================
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
완성된 JWT 예시
================
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMzQ1IiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
알고리즘 선택 가이드
====================
HS256 (HMAC + SHA-256)
- 대칭키: 같은 키로 서명/검증
- 단일 서비스, 간단한 구조에 적합
- 키 공유 필요
RS256 (RSA + SHA-256)
- 비대칭키: 개인키 서명, 공개키 검증
- 마이크로서비스, 외부 연동에 적합
- 인증 서버만 개인키 보유
ES256 (ECDSA + SHA-256)
- 비대칭키: 더 짧은 키 길이로 동등 보안
- 모바일/IoT 환경에 적합
"Access Token은 15분, Refresh Token은 7일로 설정하고, Refresh Token Rotation을 적용해서 갱신할 때마다 새 Refresh Token을 발급합니다. 탈취된 Refresh Token 재사용을 감지하면 해당 사용자의 모든 세션을 무효화합니다."
"JWT는 Base64 인코딩이지 암호화가 아닙니다. Payload에 민감한 정보를 넣으면 안 됩니다. 비밀번호, 신용카드 번호 같은 건 절대 담지 마세요. 필요하면 JWE(JSON Web Encryption)를 사용해서 페이로드를 암호화해야 합니다."
"인증 서버에서 RS256으로 JWT를 발급하면, 각 마이크로서비스는 공개키만 가지고 토큰을 검증할 수 있어요. 비밀키 공유 없이 분산 검증이 가능해서 확장성이 좋습니다. JWKS 엔드포인트로 공개키를 제공하면 키 로테이션도 용이합니다."
JWT Payload는 Base64 인코딩일 뿐 암호화가 아닙니다. 누구나 디코딩해서 내용을 볼 수 있으므로 비밀번호, 개인정보 등을 담지 마세요.
서명 알고리즘을 "none"으로 설정하면 서명 검증을 우회합니다. 라이브러리에서 허용 알고리즘 목록을 명시적으로 지정하세요.
Access Token을 너무 길게 설정하면(예: 1년) 탈취 시 오랜 기간 악용됩니다. 15분~1시간이 권장되며, Refresh Token으로 갱신하세요.
짧은 Access Token 수명 + Refresh Token Rotation, RS256 또는 ES256 사용, Payload 최소화, 만료 시간(exp) 필수 검증, 블랙리스트로 즉시 무효화 지원.