JWT(Json Web Token)의 원리와 실용적 활용

JWT란 무엇인가?

JWT(Json Web Token)는 RFC 7519에 정의된 표준으로, 네트워크 환경에서 안전하고 간결하게 정보를 전달하기 위한 방식입니다. 주로 사용자 인증이나 서비스 간 신뢰 기반 데이터 교환에 사용되며, 특히 단일 로그인(SSO), 마이크로서비스 아키텍처, 그리고 프론트엔드-백엔드 분리 구조에서 널리 채택됩니다. 이 토큰은 JSON 형식으로 구성되며 암호화된 형태로 전송되어, 수신 측에서 그 유효성을 검증할 수 있습니다.

세션 기반 인증과 토큰 기반 인증의 차이점

HTTP는 본질적으로 상태를 유지하지 않는(stateless) 프로토콜이므로, 서버는 각 요청이 누구로부터 왔는지 알 수 없습니다. 이를 해결하기 위해 세션 기반 인증에서는 사용자가 로그인하면 서버에 세션 정보를 저장하고, 브라우저에는 쿠키에 세션 ID를 부여합니다. 이후 모든 요청 시 이 쿠키를 함께 보내도록 하여 사용자를 식별합니다.

하지만 이러한 방식은 다음과 같은 문제점을 가집니다:

  • 확장성 저하: 세션 정보가 특정 서버 메모리 또는 데이터스토어에 저장되기 때문에, 여러 서버를 운영하는 분산 환경에서 부하 분산이 어려워지고, 사용자 요청이 반드시 동일한 서버로 라우팅되어야 하는 문제가 발생합니다.
  • 보안 취약점: 쿠키 기반 인증은 CSRF(Cross-Site Request Forgery) 공격에 노출되기 쉬우며, 쿠키가 탈취될 경우 인증 우회 가능성이 커집니다.
  • 서버 자원 소모: 동시 접속자가 많아질수록 서버의 세션 저장 및 관리 오버헤드가 증가합니다.

반면 토큰 기반 인증은 서버가 클라이언트에게 인증 토큰을 발급하고, 이후 모든 요청에 이 토큰을 포함시켜 전송하도록 합니다. 서버는 별도의 세션 저장소 없이 토큰 자체에 포함된 정보와 서명을 통해 유효성을 판단합니다. 이로 인해 무상태(stateless) 구조를 유지하며, 확장성과 유연성이 크게 향상됩니다.

JWS(JWT Signature)의 구조

JWT는 일반적으로 세 부분으로 나뉘며, 각각은 Base64Url 인코딩되어 점(.)으로 연결됩니다:

  1. Header (헤더): 토큰 타입과 서명 알고리즘 정보를 포함합니다.
  2. Payload (페이로드): 클레임(claim)이라 불리는 실제 데이터를 담습니다.
  3. Signature (서명): 위변조 방지를 위한 서명 값입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header 예시

다음은 JWT 헤더의 예입니다. 이 정보는 Base64Url로 인코딩됩니다.

{
  "alg": "HS256",
  "typ": "JWT"
}

2. Payload 예시

페이로드는 등록된 클레임(reserved claims), 공개 클레임(public claims), 비공개 클레임(private claims)을 포함할 수 있습니다. 민감 정보(예: 비밀번호)는 절대 포함해서는 안 됩니다.

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}
  • sub: 주체(사용자 식별자)
  • iat: 발급 시간(Unix timestamp)
  • exp: 만료 시간

3. Signature 생성 방법

서명은 다음 과정을 통해 생성됩니다:

  1. Base64Url 인코딩된 헤더와 페이로드를 마침표로 연결합니다.
  2. 결과 문자열에 비밀키(secret key)를 사용하여 HMAC-SHA256 등의 알고리즘으로 서명합니다.
  3. 서명 결과도 Base64Url로 인코딩하여 최종 JWT를 구성합니다.
signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret)

JWT 검증 과정

클라이언트가 요청 시 Authorization 헤더에 Bearer 스케마로 토큰을 포함하여 전송합니다:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxxx

서버는 다음과 같은 순서로 검증을 수행합니다:

  1. 토큰을 세 부분으로 분리합니다.
  2. 두 번째 부분(Payload)을 Base64Url 디코딩하여 만료 시간(exp) 등을 확인합니다.
  3. 첫 번째와 두 번째 부분을 다시 조합하고, 동일한 알고리즘과 비밀키로 서명을 재생성합니다.
  4. 재생성된 서명이 세 번째 부분과 일치하는지 비교하여 변조 여부를 판단합니다.

Python을 이용한 JWT 구현 예제

PyJWT 라이브러리를 사용하여 토큰 생성 및 검증을 구현할 수 있습니다.

토큰 생성

import jwt
import datetime

SECRET_KEY = 'your-super-secret-and-long-key-here'

def generate_jwt_token(user_id: int, username: str):
    payload = {
        'user_id': user_id,
        'username': username,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2),
        'iat': datetime.datetime.utcnow()
    }

    token = jwt.encode(
        payload=payload,
        key=SECRET_KEY,
        algorithm='HS256'
    )
    return token

토큰 검증

from jwt import decode, ExpiredSignatureError, DecodeError

def verify_jwt_token(token: str):
    try:
        decoded_payload = decode(token, key=SECRET_KEY, algorithms=['HS256'])
        return decoded_payload  # 인증 성공 시 페이로드 반환
    except ExpiredSignatureError:
        print("토큰이 만료되었습니다.")
        return None
    except DecodeError:
        print("토큰 해석 실패: 변조되었거나 잘못된 형식입니다.")
        return None
    except Exception as e:
        print(f"기타 인증 오류: {e}")
        return None

프레임워크 통합 예시

다양한 파이썬 웹 프레임워크에서 JWT를 쉽게 활용할 수 있습니다.

Django REST Framework - SimpleJWT

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ]
}

# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Flask - PyJWT 직접 활용

from flask import Flask, request, jsonify
import jwt

app = Flask(__name__)
SECRET_KEY = 'your-secret-key'

@app.route('/login', methods=['POST'])
def login():
    # 인증 로직 후
    token = generate_jwt_token(user_id=101, username="alice")
    return jsonify({'token': token})

@app.route('/protected')
def protected():
    auth_header = request.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return jsonify({'error': '권한이 없습니다.'}), 401

    token = auth_header.split(' ')[1]
    payload = verify_jwt_token(token)
    if not payload:
        return jsonify({'error': '유효하지 않거나 만료된 토큰입니다.'}), 401

    return jsonify({'message': f'환영합니다, {payload["username"]}!'})

보안 고려사항 및 모범 사례

  • 민감 정보 저장 금지: 페이로드는 Base64로 인코딩될 뿐 암호화되지 않으므로, 비밀번호나 개인 식별 정보는 포함하지 마세요.
  • HTTPS 필수: 토큰은 중간자 공격(MITM)에 취약하므로, 항상 HTTPS를 통해 전송해야 합니다.
  • 적절한 만료 시간 설정: Access Token은 짧게(예: 15~30분), Refresh Token은 상대적으로 길게 유지합니다.
  • 비밀키 강력하게 관리: HS256 사용 시 비밀키는 충분히 길고 무작위로 생성되어야 하며, 환경 변수 등을 통해 안전하게 주입되어야 합니다.
  • CORS 설정: 프론트엔드와의 도메인 간 요청을 허용하기 위해 적절한 CORS 정책을 구성하세요.

요약

JWT는 무상태 인증을 가능하게 하여 마이크로서비스 및 분산 시스템에서 큰 장점을 제공합니다. 간단한 구조와 낮은 전송 오버헤드 덕분에 API 보안 솔루션으로 매우 효과적이지만, 올바르게 사용하지 않으면 보안 리스크가 따릅니다. 따라서 토큰의 생명주기 관리, 적절한 알고리즘 선택, 그리고 안전한 전송 환경을 보장하는 것이 중요합니다.

태그: jwt python PyJWT authentication Security

7월 4일 18:52에 게시됨