2026년 4월 3일

JSON Web Tokens (JWT): 안전하고 효율적인 API 인증의 핵심 열쇠

90
JSON Web Tokens (JWT): 안전하고 효율적인 API 인증의 핵심 열쇠

JSON Web Tokens (JWT): 안전하고 효율적인 API 인증의 핵심 열쇠

JSON Web Tokens (JWT): 안전하고 효율적인 API 인증의 핵심 열쇠

안녕하세요, 10년차 개발자이자 기술 교육자입니다. 오늘 우리가 함께 탐구할 주제는 바로 **JSON Web Tokens (JWT)**입니다. 현대 웹 서비스, 특히 마이크로서비스 아키텍처나 모바일 애플리케이션에서 API 인증을 구현할 때 JWT는 거의 필수로 사용되는 기술입니다. 초중급 개발자라면 반드시 그 원리와 사용법을 정확히 이해하고 있어야 하죠. 면접에서도, 실무에서도 자주 접하게 될 이 중요한 개념을 쉽고 명확하게 설명해 드리겠습니다.

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

정의

JWT(JSON Web Tokens)는 RFC 7519에 정의된 웹 표준으로, 당사자 간에 정보를 JSON 객체 형태로 안전하게 전송하기 위한 간결하고 자체 포함적인(self-contained) 방법입니다. 주로 사용자 인증(Authentication) 및 권한 부여(Authorization)에 사용되지만, 어떤 정보든 안전하게 교환해야 할 때 활용될 수 있습니다.

탄생 배경

과거 웹 애플리케이션은 주로 세션(Session) 기반 인증 방식을 사용했습니다. 사용자가 로그인하면 서버는 세션 ID를 생성하고 이를 서버 메모리나 데이터베이스에 저장한 뒤, 세션 ID를 클라이언트(브라우저)에게 쿠키 형태로 전달합니다. 이후 클라이언트의 모든 요청에는 이 세션 ID가 담긴 쿠키가 함께 전송되고, 서버는 세션 ID를 통해 사용자를 식별했습니다.

하지만 이러한 세션 기반 방식은 몇 가지 한계를 가집니다.

  1. 확장성 문제 (Scalability): 서버가 여러 대로 늘어나는 분산 환경에서, 어떤 서버가 세션 정보를 가지고 있는지 모든 서버가 공유해야 하는 문제(세션 스티키니스 또는 중앙 세션 저장소 필요)가 발생하여 수평 확장에 어려움이 있었습니다.
  2. CSRF 취약점: 쿠키 기반이므로 CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있습니다.
  3. 모바일 앱/크로스 도메인 문제: 웹 브라우저가 아닌 모바일 앱이나, API 서버와 클라이언트 앱의 도메인이 다를 경우 쿠키 기반 인증은 복잡해지거나 제한적일 수 있습니다.
  4. 무상태(Stateless) API의 필요성: RESTful API는 본질적으로 무상태(Stateless)를 지향합니다. 즉, 서버는 클라이언트의 상태를 저장하지 않고 요청 자체만으로 모든 정보를 처리할 수 있어야 합니다. 세션 기반은 서버가 클라이언트의 상태(로그인 여부)를 저장해야 하므로 무상태 원칙에 위배됩니다.

이러러한 문제들을 해결하기 위해 JWT가 등장했습니다. JWT는 서버가 클라이언트의 상태를 저장할 필요 없는 무상태(Stateless) 인증 방식을 제공하여, 분산 시스템과 모바일 환경에 더욱 적합한 대안이 되었습니다.

왜 중요한가?

JWT가 중요한 이유는 다음과 같습니다.

  • 무상태(Stateless) 서버: 서버가 클라이언트의 인증 정보를 저장할 필요 없이, 토큰 자체만으로 유효성을 검증할 수 있어 서버의 부담을 줄이고 수평적 확장을 매우 용이하게 합니다. 이는 마이크로서비스 아키텍처에 특히 유리합니다.
  • 보안성: 토큰이 디지털 서명되어 있으므로, 토큰이 중간에 탈취되더라도 내용이 위변조되었는지 서버에서 쉽게 검증할 수 있습니다.
  • 간결성 및 범용성: JSON 형태로 이루어져 있어 다양한 프로그래밍 언어에서 쉽게 파싱하고 처리할 수 있습니다. HTTP 헤더를 통해 전달되므로 쿠키 의존성 없이 모바일 앱, 크로스 도메인 환경에서도 유연하게 사용할 수 있습니다.
  • 자가 포함성(Self-contained): 토큰 내부에 사용자 정보(Payload)를 포함하고 있어, 서버가 매번 데이터베이스를 조회하지 않고도 필요한 정보를 얻을 수 있습니다.

2. 핵심 원리 설명: 구조와 동작 방식

2. 핵심 원리 설명: 구조와 동작 방식

JWT는 크게 세 부분으로 나뉘며, 각 부분은 점(.)으로 구분됩니다. Header.Payload.Signature

1) Header (헤더)

헤더는 토큰의 타입과 서명에 사용될 알고리즘을 명시합니다.

{
  "alg": "HS256", // 서명 알고리즘 (HMAC SHA256)
  "typ": "JWT"    // 토큰 타입
}

이 JSON 객체는 Base64 URL-safe 방식으로 인코딩되어 JWT의 첫 번째 부분이 됩니다.

2) Payload (페이로드)

페이로드는 토큰에 담을 정보, 즉 '클레임(Claim)'을 포함합니다. 클레임은 key:value 쌍으로 이루어진 JSON 객체입니다. 클레임의 종류는 세 가지가 있습니다.

  • 등록된 클레임 (Registered Claims): 토큰에 대한 정보를 담기 위해 미리 정의된 클레임들입니다. 예를 들어 iss (발급자), exp (만료 시간), sub (주제), aud (수신자) 등이 있습니다. 이들은 필수는 아니지만, 상호 운용성을 위해 사용하는 것이 권장됩니다.
  • 공개 클레임 (Public Claims): JWT 사용자들이 충돌이 없도록 정의한 클레임입니다. IANA JWT Registry에 등록하거나, URI 형태로 정의하여 충돌을 방지합니다.
  • 비공개 클레임 (Private Claims): 서버와 클라이언트 간에 협의하여 사용하는 클레임입니다. 예를 들어 사용자 ID, 사용자 역할(role) 등 애플리케이션에 특화된 정보를 담을 수 있습니다.
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1678886400 // 만료 시간 (Unix Time Stamp)
}

이 JSON 객체 또한 Base64 URL-safe 방식으로 인코딩되어 JWT의 두 번째 부분이 됩니다. 주의할 점은 페이로드는 인코딩될 뿐 암호화되지 않는다는 것입니다. 따라서 민감한 정보는 절대 페이로드에 직접 넣어서는 안 됩니다.

3) Signature (서명)

서명은 JWT의 무결성과 신뢰성을 보장하는 부분입니다. Header와 Payload를 Base64 URL-safe 인코딩한 값과 서버만이 아는 **비밀 키(Secret Key)**를 사용하여 서명합니다.

서명 생성 과정은 다음과 같습니다. HMACSHA256( Base64UrlEncode(header) + "." + Base64UrlEncode(payload), secret_key )

이렇게 생성된 서명은 JWT의 세 번째 부분이 됩니다. 서버는 토큰을 받을 때, 동일한 방식으로 서명을 다시 계산하여 토큰에 포함된 서명과 일치하는지 확인합니다. 만약 일치하지 않는다면 토큰이 위변조되었다고 판단하고 요청을 거부합니다.

JWT 동작 방식 (비유: 신분증)

JWT는 마치 위조 불가능한 신분증과 같습니다.

  1. 로그인 (신분증 발급):

    • 사용자가 웹사이트에 ID와 비밀번호로 로그인합니다.
    • 서버는 사용자를 인증한 후, 해당 사용자의 정보(예: 사용자 ID, 권한)를 담은 JWT를 생성합니다. 이때 서버만이 아는 **비밀 도장(Secret Key)**으로 JWT에 서명하여 위조 방지 홀로그램을 새깁니다.
    • 서버는 이 서명된 JWT(신분증)를 클라이언트(사용자)에게 발급합니다.
  2. 토큰 저장 (신분증 소지):

    • 클라이언트는 발급받은 JWT(신분증)를 안전하게 저장합니다 (예: 웹 브라우저의 로컬 스토리지).
  3. API 요청 (신분증 제시):

    • 클라이언트가 보호된 리소스(예: 내 프로필 정보, 게시글 작성)를 요청할 때마다, HTTP 요청의 Authorization 헤더에 JWT(신분증)를 첨부하여 서버로 보냅니다. 보통 Bearer <token> 형식으로 보냅니다.
  4. 토큰 검증 (신분증 확인):

    • 서버는 클라이언트로부터 받은 JWT(신분증)를 받으면, 먼저 서버만이 아는 **비밀 도장(Secret Key)**으로 서명(홀로그램)을 확인하여 신분증이 위조되지 않았는지 검증합니다.
    • 서명이 유효하면, 토큰 내부에 담긴 사용자 정보(이름, 권한, 유효기간 등)를 확인하여 해당 사용자가 요청한 리소스에 접근할 권한이 있는지, 그리고 토큰이 만료되지 않았는지 등을 판단합니다.
    • 이 모든 검증 과정은 서버가 별도의 세션 정보를 저장하지 않고 토큰 자체만으로 이루어집니다.
  5. 응답 (서비스 제공):

    • 검증이 성공하면 서버는 요청된 리소스를 클라이언트에게 제공합니다.
sequenceDiagram
    participant C as Client (Browser/Mobile App)
    participant S as Server (API Server)
    participant DB as Database

    C->>S: 1. 로그인 요청 (ID, Password)
    S->>DB: 2. 사용자 인증
    DB-->>S: 3. 인증 성공
    S->>S: 4. JWT 생성 (Payload + Secret Key로 서명)
    S-->>C: 5. JWT 응답 (Access Token)
    C->>C: 6. JWT 저장 (Local Storage 등)

    C->>S: 7. 보호된 리소스 요청 (Authorization: Bearer <JWT>)
    S->>S: 8. JWT 서명 검증 (Secret Key 사용)
    alt 서명 유효 & 토큰 미만료
        S->>S: 9. Payload 디코딩 및 권한 확인
        S-->>C: 10. 리소스 제공
    else 서명 위변조 또는 토큰 만료
        S-->>C: 10. 에러 응답 (401 Unauthorized)
    end

3. 코드 예제 2개 (Python)

Python에서 JWT를 다루기 위해 PyJWT 라이브러리를 사용합니다. pip install PyJWT로 설치할 수 있습니다.

예제 1: JWT 생성

이 예제는 사용자 ID와 관리자 여부 정보를 담은 JWT를 생성합니다. secret_key는 실제 서비스에서는 환경 변수 등으로 관리해야 합니다.

import jwt
import datetime
import time

# 실제 서비스에서는 이 키를 안전하게 관리해야 합니다.
# 환경 변수, KMS 등에서 로드하는 것을 권장합니다.
SECRET_KEY = "your-super-secret-key-that-no-one-should-know"

def generate_jwt(user_id: str, is_admin: bool, expiry_minutes: int = 30) -> str:
    """
    사용자 정보를 담은 JWT를 생성합니다.
    Args:
        user_id: 사용자 고유 ID
        is_admin: 관리자 여부
        expiry_minutes: 토큰 만료 시간 (분 단위)
    Returns:
        생성된 JWT 문자열
    """
    # JWT Payload (클레임) 정의
    # 'exp': 토큰 만료 시간 (Unix Time Stamp)
    # 'iat': 토큰 발급 시간 (Issued At)
    # 'sub': 토큰의 주제 (Subject) - 여기서는 user_id
    # 'admin': 비공개 클레임, 관리자 여부
    payload = {
        "user_id": user_id,
        "admin": is_admin,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=expiry_minutes),
        "iat": datetime.datetime.utcnow(),
        "sub": user_id
    }

    # JWT 생성
    # HS256 알고리즘 사용 (Header에 "alg": "HS256"로 자동 포함)
    token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
    return token

# JWT 생성 예시
user_id = "testuser123"
is_admin = False
access_token = generate_jwt(user_id, is_admin)
print(f"생성된 JWT: {access_token}")

# 관리자용 JWT 생성 예시
admin_id = "admin_user"
admin_token = generate_jwt(admin_id, True, expiry_minutes=60) # 관리자 토큰은 1시간 유효
print(f"생성된 관리자 JWT: {admin_token}")

# 생성된 JWT의 구조를 확인하기 위해 디코딩 (서명 검증 없이)
# 실제 환경에서는 이처럼 서명 검증 없이 디코딩하는 것은 보안상 위험합니다.
# 단지 구조 확인을 위한 용도입니다.
decoded_payload_without_verification = jwt.decode(access_token, options={"verify_signature": False})
print(f"서명 검증 없이 디코딩된 페이로드 (구조 확인용): {decoded_payload_without_verification}")

예제 2: JWT 검증 및 Payload 추출

이 예제는 클라이언트로부터 받은 JWT를 검증하고, 유효한 경우 페이로드에서 사용자 정보를 추출합니다.

import jwt
import datetime
import time

SECRET_KEY = "your-super-secret-key-that-no-one-should-know"

def verify_jwt(token: str) -> dict | None:
    """
    JWT를 검증하고 유효한 경우 페이로드를 반환합니다.
    Args:
        token: 검증할 JWT 문자열
    Returns:
        페이로드 딕셔너리 또는 None (유효하지 않은 경우)
    """
    try:
        # JWT 디코딩 및 서명 검증, 만료 시간 검증
        # algorithms: 토큰 생성 시 사용된 알고리즘을 명시해야 합니다.
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        print("토큰이 만료되었습니다.")
        return None
    except jwt.InvalidTokenError:
        print("유효하지 않은 토큰입니다. 서명이 일치하지 않거나 형식이 잘못되었습니다.")
        return None

# 이전 예제에서 생성된 토큰 사용
# 만료된 토큰을 시뮬레이션하기 위해 짧은 유효 기간으로 토큰을 다시 생성합니다.
expired_token = generate_jwt("expired_user", False, expiry_minutes=0.01) # 거의 즉시 만료
time.sleep(1) # 1초 대기하여 토큰이 확실히 만료되도록 함

valid_token = generate_jwt("valid_user", True, expiry_minutes=1) # 1분 유효

# JWT 검증 예시
print("\n--- 유효한 토큰 검증 ---")
verified_payload = verify_jwt(valid_token)
if verified_payload:
    print(f"검증 성공! 사용자 ID: {verified_payload['user_id']}, 관리자 여부: {verified_payload['admin']}")
else:
    print("유효한 토큰 검증 실패.")

print("\n--- 만료된 토큰 검증 ---")
verified_payload_expired = verify_jwt(expired_token)
if verified_payload_expired:
    print(f"검증 성공! 사용자 ID: {verified_payload_expired['user_id']}")
else:
    print("만료된 토큰 검증 실패 (정상 동작).")

print("\n--- 위변조된 토큰 검증 ---")
# 토큰의 일부를 변경하여 위변조 시도 (Signature가 일치하지 않게 됨)
tampered_token = valid_token + "a"
verified_payload_tampered = verify_jwt(tampered_token)
if verified_payload_tampered:
    print(f"검증 성공! 사용자 ID: {verified_payload_tampered['user_id']}")
else:
    print("위변조된 토큰 검증 실패 (정상 동작).")

4. 실무 적용 사례

JWT는 현대 웹 개발에서 다양한 방식으로 활용됩니다.

  • API 인증: 가장 일반적인 사용 사례입니다. 클라이언트(웹, 모바일 앱)가 백엔드 API 서버에 접근할 때 JWT를 Authorization 헤더에 담아 전송하여 사용자를 인증하고 권한을 부여합니다. 특히 마이크로서비스 아키텍처에서 각 서비스가 독립적으로 JWT를 검증할 수 있어 서비스 간 의존성을 줄이고 확장성을 높이는 데 기여합니다.
  • 싱글 사인 온 (SSO - Single Sign-On): 여러 애플리케이션이나 서비스에서 한 번의 인증으로 모두 접근할 수 있도록 하는 시스템에 사용됩니다. 사용자가 한 서비스에서 로그인하면 JWT가 발급되고, 이 토큰을 다른 서비스에 전달하여 별도의 로그인 없이 접근할 수 있게 합니다.
  • 정보 교환: 두 당사자(예: 서버 간) 사이에 특정 정보를 안전하게 교환해야 할 때 사용될 수 있습니다. 예를 들어, 이메일 주소 확인 링크나 비밀번호 재설정 링크에 사용자 ID와 만료 시간을 담은 JWT를 포함하여 보낼 수 있습니다.
  • OAuth 2.0과 연동: OAuth 2.0은 인증(Authentication)보다는 권한 부여(Authorization)를 위한 프레임워크입니다. OAuth 2.0에서 발급하는 Access Token이나 OpenID Connect(OAuth 2.0 기반의 인증 레이어)에서 발급하는 ID Token은 종종 JWT 형태로 구현됩니다. 이를 통해 클라이언트는 토큰 자체만으로 사용자 정보를 얻고 유효성을 검증할 수 있습니다.

5. 자주 하는 실수와 해결법

JWT는 강력하지만, 잘못 사용하면 심각한 보안 문제를 야기할 수 있습니다. 다음은 개발자들이 자주 저지르는 실수와 그 해결책입니다.

  1. Secret Key 노출:

    • 문제: JWT 서명에 사용되는 Secret Key가 외부에 노출되면, 공격자가 임의의 JWT를 생성하거나 기존 토큰을 위변조할 수 있습니다. 이는 시스템의 보안을 완전히 무너뜨리는 치명적인 실수입니다.
    • 해결법: Secret Key는 절대로 소스코드에 하드코딩하거나 Git 같은 버전 관리 시스템에 올리지 마세요. 반드시 환경 변수, 클라우드 서비스의 비밀 관리 서비스(AWS KMS, Azure Key Vault, Google Secret Manager), HashiCorp Vault 같은 안전한 저장소에 보관하고, 애플리케이션 시작 시 로드하도록 해야 합니다. 주기적으로 키를 변경하는 정책을 수립하는 것도 좋습니다.
  2. Payload에 민감 정보 저장:

    • 문제: JWT의 Payload는 Base64 인코딩될 뿐, 암호화되지 않습니다. 따라서 누구나 쉽게 디코딩하여 내용을 확인할 수 있습니다. 주민등록번호, 비밀번호, 개인 식별이 가능한 상세 정보(PII) 등을 Payload에 저장하면 정보 유출의 위험이 있습니다.
    • 해결법: Payload에는 사용자 ID, 권한, 토큰 만료 시간 등 최소한의 정보만 담아야 합니다. 민감한 정보는 서버의 데이터베이스에 저장하고, JWT의 사용자 ID를 이용해 필요한 시점에 DB에서 조회하는 방식으로 처리해야 합니다.
  3. 토큰 탈취에 대한 대비 부족:

    • 문제: JWT는 탈취당하면 해당 토큰의 유효 기간 동안 공격자가 사용자 행세를 할 수 있습니다.
    • 해결법:
      • HTTPS 사용 필수: 모든 통신은 반드시 HTTPS를 통해 암호화되어야 합니다. HTTP 사용 시 중간자 공격(Man-in-the-Middle Attack)으로 토큰이 쉽게 탈취될 수 있습니다.
      • 짧은 유효 기간 설정: exp 클레임을 사용하여 토큰의 유효 기간을 짧게 (예: 15분~1시간) 설정합니다. 토큰 탈취 시 피해를 최소화할 수 있습니다.
      • Refresh Token 활용: Access Token의 유효 기간을 짧게 하고, 유효 기간이 긴 Refresh Token을 사용하여 Access Token이 만료되면 재발급받는 방식을 사용합니다. Refresh Token은 Access Token보다 더 안전한 방식으로 저장하고 관리해야 합니다 (예: HttpOnly 쿠키).
      • 토큰 저장 위치: 클라이언트 측에서 JWT를 localStorage에 저장하는 것은 XSS(Cross-Site Scripting) 공격에 취약할 수 있습니다. HttpOnly 속성을 가진 쿠키에 저장하는 것이 XSS로부터 더 안전하다고 알려져 있지만, CSRF에 취약해질 수 있습니다. 각각의 장단점을 이해하고 애플리케이션의 보안 요구사항에 맞춰 선택해야 합니다. 일반적으로 localStorage는 XSS 방지 노력이 확실하다면 괜찮은 선택으로 여겨집니다.
  4. 서명 검증 누락 또는 잘못된 알고리즘 사용:

    • 문제: 서버가 JWT를 받을 때 서명을 제대로 검증하지 않거나, 토큰 헤더의 alg 필드를 무조건 신뢰하여 None이나 약한 알고리즘을 사용하도록 허용하는 경우, 공격자가 임의의 토큰을 서명 없이 생성하여 서버를 속일 수 있습니다. (흔히 "None 알고리즘 취약점"으로 불림)
    • 해결법: JWT 라이브러리를 사용할 때, 반드시 algorithms 매개변수에 허용할 서명 알고리즘을 명시해야 합니다. 서버는 토큰 헤더의 alg 값을 무조건 신뢰하지 않고, 자신이 예상하는 알고리즘(예: HS256, RS256)만 허용해야 합니다.
  5. JWT Revocation (취소) 문제:

    • 문제: JWT는 무상태이므로, 한 번 발급된 유효한 토큰은 서버에서 강제로 만료시키거나 취소하기 어렵습니다. 사용자가 로그아웃하거나 비밀번호를 변경해도, 유효 기간이 남은 토큰은 계속 유효하게 됩니다.
    • 해결법:
      • 짧은 유효 기간 + Refresh Token: 위에서 언급했듯이 Access Token의 유효 기간을 짧게 설정하고, Refresh Token으로 관리하여 Access Token 만료 시점에만 재인증 과정을 거치도록 합니다.
      • 블랙리스트/화이트리스트: 로그아웃, 비밀번호 변경 등으로 무효화해야 할 토큰들을 Redis 같은 빠른 인메모리 데이터베이스에 블랙리스트로 저장하고, 요청이 올 때마다 해당 토큰이 블랙리스트에 있는지 확인합니다. (이 방법은 무상태라는 JWT의 장점을 일부 상실하게 하지만, 보안을 위해 필요할 수 있습니다.)
      • 세션 관리: 토큰 자체는 무상태로 유지하되, 서버에서 사용자별 세션 ID를 관리하고 JWT 페이로드에 세션 ID를 포함시켜, 세션 ID가 유효한지 추가로 확인하는 방식도 있습니다.

6. 더 공부할 리소스 추천

JWT는 현대 웹 개발의 필수 요소이므로, 더 깊이 있는 이해를 위해 다음 자료들을 참고하시길 권장합니다.

  • RFC 7519 (JSON Web Token): JWT의 공식 표준 문서입니다. 기술적인 세부 사항을 정확히 이해하는 데 도움이 됩니다. (영문)
  • jwt.io: JWT를 시각적으로 디버깅하고 이해하는 데 매우 유용한 웹사이트입니다. 토큰을 붙여넣으면 헤더, 페이로드, 서명 부분을 분석해 줍니다.
  • Auth0 블로그: Auth0는 인증 및 권한 부여 솔루션을 제공하는 회사로, JWT를 비롯한 다양한 인증 기술에 대한 훌륭한 설명과 튜토리얼을 제공합니다. (영문)
  • OpenID Connect: JWT를 ID 토큰으로 사용하여 사용자 인증 정보를 교환하는 표준입니다. OAuth 2.0과 함께 현대 인증 시스템의 중요한 축을 이룹니다.
  • 사용하는 언어의 JWT 라이브러리 공식 문서: Python의 PyJWT, JavaScript의 jsonwebtoken 등 각 언어별 라이브러리 문서를 통해 실제 구현 방법을 익히세요.

JWT는 한 번 제대로 이해하면 분산 시스템과 API 개발에 큰 자신감을 줄 수 있는 기술입니다. 꾸준히 학습하고 실습하여 여러분의 역량을 한 단계 더 끌어올리시길 바랍니다!