Python JWT 디코딩 PyJWT 가이드
무료 JWT 디코더을 브라우저에서 직접 사용하세요 — 설치 불필요.
JWT 디코더 온라인으로 사용하기 →토큰 기반 인증을 사용하는 모든 API는 어느 시점에 JWT를 반환하고, 그 내용을 파악하는 것은 개발 중에 반복해서 필요한 작업입니다. Python JWT 디코더는 불투명한 base64 문자열을 실제로 작업할 수 있는 가독성 있는 클레임 딕셔너리로 변환해 줍니다. 사용할 PyPI 패키지는 PyJWT로, pip install PyJWT로 설치하지만 import jwt로 임포트합니다. 이 가이드에서는 jwt.decode()를 사용한 완전한 서명 검증, 빠른 점검을 위한 시크릿 없이 디코딩, 라이브러리 없이 base64로 수동 디코딩, RS256 공개 키 검증, 그리고 프로덕션 인증 시스템에서 자주 만나는 실수들을 다룹니다. 빠르게 한 번 확인하려면 온라인 JWT 디코더를 사용하면 코드 없이 즉시 확인할 수 있습니다. 모든 예제는 Python 3.10+과 PyJWT 2.x를 기준으로 합니다.
- ✓pip install PyJWT로 설치하고 import jwt로 임포트합니다 — 패키지 이름과 임포트 이름이 달라 거의 모든 사람이 처음에 혼동합니다.
- ✓jwt.decode(token, key, algorithms=["HS256"])는 클레임이 담긴 딕셔너리를 반환합니다. 항상 algorithms를 명시적으로 전달하세요.
- ✓검증 없이 클레임을 확인하려면: jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256"]).
- ✓RSA/EC 토큰의 경우: pip install PyJWT cryptography — 비대칭 알고리즘에는 cryptography 백엔드가 필요합니다.
- ✓수동 디코딩(base64 + json)은 라이브러리 없이도 작동하지만 서명 및 만료 검증을 모두 건너뜁니다.
JWT 디코딩이란?
JSON Web Token은 점(.)으로 구분된 세 개의 base64url 인코딩 세그먼트로 구성됩니다: 헤더 (알고리즘과 토큰 타입), 페이로드(클레임 — 사용자 ID, 역할, 만료 시각), 서명이 그것입니다. JWT 디코딩은 헤더와 페이로드 세그먼트를 추출하고, base64url로 디코딩한 뒤, 결과 JSON을 클레임 딕셔너리로 파싱하는 과정입니다.
헤더는 토큰 서명에 사용된 알고리즘과 경우에 따라 올바른 검증 키를 찾기 위한 kid (키 ID)를 포함합니다. 페이로드에는 실제 데이터가 담겨 있습니다: 토큰 발급 대상(sub), 만료 시각(exp), 대상 서비스(aud), 그리고 애플리케이션이 정의한 커스텀 클레임이 포함됩니다. 서명 세그먼트는 토큰이 변조되지 않았음을 증명하지만, 검증하려면 시크릿 키 또는 공개 키가 필요합니다. 디코딩과 검증은 별개의 작업입니다. 서명을 검증하지 않고도 페이로드를 디코딩할 수 있지만(디버깅에 유용), 검증되지 않은 클레임을 인가 결정에 신뢰해서는 안 됩니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTgxNTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
{
"sub": "usr_8f2a",
"role": "admin",
"exp": 1711815600
}jwt.decode() — PyJWT로 디코딩 및 검증
jwt.decode()는 PyJWT 라이브러리의 핵심 함수입니다. 인코딩된 토큰 문자열, 시크릿 키(HMAC 알고리즘의 경우) 또는 공개 키(RSA/EC의 경우), 그리고 필수인 algorithms 목록을 받습니다. 이 함수는 서명을 검증하고 exp와 nbf 같은 표준 클레임을 확인하며, 페이로드를 Python 딕셔너리로 반환합니다. 서명 오류, 만료된 토큰, 잘못된 알고리즘 등 어떤 문제가 발생해도 구체적인 예외를 발생시킵니다.
최소 동작 예제
import jwt
# A shared secret between the issuer and this service
SECRET_KEY = "k8s-webhook-signing-secret-2026"
# Encode a token first (simulating what an auth server would issue)
token = jwt.encode(
{"sub": "usr_8f2a", "role": "admin", "team": "platform"},
SECRET_KEY,
algorithm="HS256"
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3Jf...
# Decode and verify the token
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
print(payload)
# {'sub': 'usr_8f2a', 'role': 'admin', 'team': 'platform'}
print(payload["role"])
# adminalgorithms 파라미터는 단일 문자열이 아닌 목록이며, PyJWT 2.x에서는 필수입니다. 이는 보안 기능입니다: 명시하지 않으면 공격자가 헤더에 alg: none을 지정한 토큰을 만들어 검증을 완전히 우회할 수 있습니다. 항상 애플리케이션이 허용하는 알고리즘을 정확히 명시하세요. HS256 토큰만 발급한다면 목록은 ["HS256"]이어야 합니다 — ["HS256", "RS256", "none"]이 아닙니다. 목록을 좁게 유지할수록 공격 표면이 줄어듭니다.
초반에 저를 혼란스럽게 했던 점이 있는데: PyJWT 2.x에서는 jwt.encode()가 바이트 대신 문자열을 반환하도록 변경되었습니다. 인코딩된 토큰에 .decode("utf-8")를 호출하는 오래된 Stack Overflow 답변들은 PyJWT 1.x 시대의 코드이며, 버전 2.x에서는 AttributeError를 발생시킵니다. 토큰은 이미 문자열이므로 그대로 사용하면 됩니다.
만료 시각이 포함된 전체 라운드트립
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "webhook-processor-secret"
# Create a token that expires in 1 hour
payload = {
"sub": "svc_payment_processor",
"iss": "auth.internal.example.com",
"aud": "https://api.example.com",
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
"permissions": ["orders:read", "refunds:create"],
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
# Later, when the token arrives in a request header:
try:
decoded = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
audience="https://api.example.com",
issuer="auth.internal.example.com",
)
print(f"Service: {decoded['sub']}")
print(f"Permissions: {decoded['permissions']}")
except jwt.ExpiredSignatureError:
print("Token expired — request re-authentication")
except jwt.InvalidAudienceError:
print("Token not intended for this API")
except jwt.InvalidIssuerError:
print("Token issued by unknown authority")datetime 객체를 Unix 타임스탬프로 자동 변환합니다. 디코딩 시에는 exp, iat, 그리고 nbf 클레임이 datetime 객체가 아닌 정수로 반환됩니다. 직접 datetime.fromtimestamp(payload["exp"], tz=timezone.utc)로 변환해야 합니다.서명 검증 없이 JWT 디코딩
토큰을 검증하기 전에 클레임을 읽어야 하는 경우가 있습니다. 흔한 시나리오: 토큰 헤더에 kid (키 ID) 필드가 있고, 검증 전에 JWKS 엔드포인트에서 해당 공개 키를 가져와야 하는 경우입니다. PyJWT는 verify_signature: False 옵션으로 이를 지원합니다. algorithms 목록은 여전히 전달하지만 key 인수는 무시됩니다.
import jwt
token = (
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZy0xNzI2In0"
".eyJzdWIiOiJ1c3JfM2M3ZiIsInNjb3BlIjoicmVhZDpvcmRlcnMiLCJpc3MiOiJhdXRoLmV4YW1wbGUuY29tIn0"
".signature_placeholder"
)
# Step 1: Read claims without verification to get routing info
unverified = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["RS256"]
)
print(unverified)
# {'sub': 'usr_3c7f', 'scope': 'read:orders', 'iss': 'auth.example.com'}
# Step 2: Read the header to find which key to use
header = jwt.get_unverified_header(token)
print(header)
# {'alg': 'RS256', 'typ': 'JWT', 'kid': 'sig-1726'}
# Now use header['kid'] to fetch the correct public key from your JWKS endpoint여기에 미묘한 차이가 있습니다. jwt.get_unverified_header()는 헤더(첫 번째 세그먼트)만 읽습니다. verify_signature: False를 사용한 jwt.decode() 호출은 페이로드(두 번째 세그먼트)를 읽습니다. 두 함수를 합치면 키 없이 토큰에서 모든 것을 추출할 수 있습니다. PyJWT는 서명 검증이 꺼져 있을 때도 토큰의 구조(점으로 구분된 세 개의 세그먼트, 유효한 base64, 유효한 JSON)를 확인합니다. 토큰의 구조가 잘못된 경우 어떤 옵션을 전달하더라도 DecodeError를 발생시킵니다.
jwt.decode() 파라미터 레퍼런스
전체 시그니처는 jwt.decode(jwt, key, algorithms, options, audience, issuer, leeway, require)입니다. algorithms 이후의 모든 파라미터는 키워드 전용입니다.
options 딕셔너리는 PyJWT가 수행하는 각 검증을 세밀하게 제어합니다. 키들은 개별 검사에 매핑됩니다: verify_signature, verify_exp, verify_nbf, verify_iss, verify_aud, 그리고 verify_iat. 명시적으로 False로 설정하지 않는 한 모두 기본값은 True입니다. 프로덕션에서는 모두 기본값으로 두세요. 개별 검사를 비활성화하는 것은 오래된 테스트 토큰으로 작업하면서 만료를 일시적으로 우회해야 하는 개발 단계에서만 합니다.
import jwt
# Require specific claims to be present — raises MissingRequiredClaimError if absent
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
options={"require": ["exp", "iss", "sub"]},
issuer="auth.internal.example.com",
)
# During development only: skip expiry to test with old tokens
dev_payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
options={"verify_exp": False}, # DO NOT use in production
)base64와 json으로 수동 JWT 디코딩
Python 표준 라이브러리만으로도 JWT 페이로드를 디코딩할 수 있습니다 — pip install 불필요. 다음 상황에서 진정으로 유용합니다: 의존성 추가가 과한 디버깅 스크립트, 표준 라이브러리만 사용 가능한 제한된 CI 환경, 콜드 스타트 시간을 최소화하고 싶은 AWS Lambda 함수, 또는 JWT의 내부 구조를 이해하고 싶을 때. 과정은 간단합니다: 점으로 분리하고, 원하는 세그먼트를 가져와서, base64 패딩을 추가하고, 디코딩한 뒤 JSON을 파싱합니다.
base64와 json 모듈 모두 Python 표준 라이브러리에 포함되어 있어 Python 3.6 이상의 모든 환경에서 작동합니다. 아래 함수들은 헤더(첫 번째 세그먼트)와 페이로드(두 번째 세그먼트)를 각각 처리합니다:
import base64
import json
def decode_jwt_payload(token: str) -> dict:
"""Decode the JWT payload without signature verification.
Works with any JWT — HS256, RS256, ES256, etc.
"""
parts = token.split(".")
if len(parts) != 3:
raise ValueError(f"Expected 3 JWT segments, got {len(parts)}")
payload_b64 = parts[1]
# base64url uses - and _ instead of + and /
# Python's urlsafe_b64decode handles this, but needs padding
payload_b64 += "=" * (-len(payload_b64) % 4)
payload_bytes = base64.urlsafe_b64decode(payload_b64)
return json.loads(payload_bytes)
def decode_jwt_header(token: str) -> dict:
"""Decode the JWT header (algorithm, key ID, type)."""
header_b64 = token.split(".")[0]
header_b64 += "=" * (-len(header_b64) % 4)
return json.loads(base64.urlsafe_b64decode(header_b64))
# Example usage
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTgxNTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
header = decode_jwt_header(token)
print(f"Algorithm: {header['alg']}")
# Algorithm: HS256
claims = decode_jwt_payload(token)
print(f"Subject: {claims['sub']}")
print(f"Role: {claims['role']}")
# Subject: usr_8f2a
# Role: admin패딩 트릭(+= "=" * (-len(s) % 4))은 모두가 잊어버리는 부분입니다. JWT base64url은 끝의 = 문자를 생략하지만 Python의 urlsafe_b64decode는 이를 요구합니다. 패딩 수정 없이는 binascii.Error: Incorrect padding이 발생합니다.
jwt.decode()를 사용하세요.API 응답 및 토큰 파일에서 JWT 디코딩
두 가지 가장 흔한 실제 시나리오: HTTP 응답에서 JWT 추출(OAuth 토큰 엔드포인트, 로그인 API), 그리고 파일에서 토큰 읽기(서비스 계정 자격증명, Kubernetes 마운트된 시크릿, 디스크에 캐시된 토큰). 두 경우 모두 적절한 예외 처리가 필요합니다. 네트워크 요청은 실패할 수 있습니다. 파일이 없을 수도 있습니다. 캐시된 후 읽기 전에 토큰이 만료될 수 있습니다.
아래 예제에서는 HTTP 호출에 httpx를 사용합니다 (선호한다면 requests로 교체해도 패턴은 동일합니다), 파일 작업에는 pathlib.Path를 사용합니다. 각 예제는 단순한 except Exception 대신 구체적인 PyJWT 예외를 처리하여, 각 오류 상황에 적절히 대응할 수 있습니다: 만료 시 재인증, 서명 실패 시 알림, 네트워크 타임아웃 시 재시도.
API 응답에서 JWT 디코딩
import jwt
import httpx # or requests
TOKEN_ENDPOINT = "https://auth.example.com/oauth/token"
SECRET_KEY = "shared-webhook-signing-key"
def get_and_decode_token() -> dict:
"""Fetch an access token from the auth server and decode it."""
try:
response = httpx.post(
TOKEN_ENDPOINT,
data={
"grant_type": "client_credentials",
"client_id": "svc_order_processor",
"client_secret": "cs_9f3a7b2e",
},
timeout=10.0,
)
response.raise_for_status()
except httpx.HTTPError as exc:
raise RuntimeError(f"Token request failed: {exc}") from exc
token_data = response.json()
access_token = token_data["access_token"]
try:
payload = jwt.decode(
access_token,
SECRET_KEY,
algorithms=["HS256"],
audience="https://api.example.com",
)
return payload
except jwt.InvalidTokenError as exc:
raise RuntimeError(f"Invalid token from auth server: {exc}") from exc
claims = get_and_decode_token()
print(f"Service: {claims['sub']}, Scopes: {claims.get('scope', 'none')}")파일에서 JWT 디코딩
import jwt
from pathlib import Path
from datetime import datetime, timezone
TOKEN_PATH = Path("/var/run/secrets/service-account-token")
PUBLIC_KEY_PATH = Path("/etc/ssl/auth/public_key.pem")
def decode_token_from_file() -> dict:
"""Read a JWT from a file, verify with a PEM public key."""
try:
token = TOKEN_PATH.read_text().strip()
public_key = PUBLIC_KEY_PATH.read_text()
except FileNotFoundError as exc:
raise RuntimeError(f"Missing file: {exc.filename}") from exc
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://internal-api.example.com",
)
except jwt.ExpiredSignatureError:
exp_time = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["RS256"],
).get("exp", 0)
expired_at = datetime.fromtimestamp(exp_time, tz=timezone.utc)
raise RuntimeError(f"Token expired at {expired_at.isoformat()}")
except jwt.InvalidTokenError as exc:
raise RuntimeError(f"Token verification failed: {exc}") from exc
return payload
claims = decode_token_from_file()
print(f"Subject: {claims['sub']}, Issuer: {claims['iss']}")커맨드라인 JWT 디코딩
때로는 스크립트를 작성하지 않고 터미널에서 바로 토큰 내용을 확인해야 할 때가 있습니다. OAuth 흐름을 디버깅하면서 Authorization 헤더 내용을 보거나, 브라우저 DevTools에서 토큰을 가져와 만료 시각을 확인하고 싶을 때가 있습니다. Python의 -c 플래그로 한 줄 명령어를 만들 수 있습니다. 토큰을 파이프로 전달하면 클레임이 포맷된 JSON으로 출력됩니다. 스크립트 파일도 가상 환경도 필요 없습니다.
# Decode JWT payload from clipboard or variable (no verification)
echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiJ9.sig" \
| python3 -c "
import sys, base64, json
token = sys.stdin.read().strip()
payload = token.split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
"
# {
# "sub": "usr_8f2a",
# "role": "admin"
# }# Decode JWT header to check algorithm and key ID
echo "eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0xNzI2In0.payload.sig" \
| python3 -c "
import sys, base64, json
token = sys.stdin.read().strip()
header = token.split('.')[0]
header += '=' * (-len(header) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(header)), indent=2))
"
# {
# "alg": "RS256",
# "kid": "sig-1726"
# }# If PyJWT is installed, verify and decode in one step
python3 -c "
import jwt, sys, json
token = sys.argv[1]
payload = jwt.decode(token, options={'verify_signature': False}, algorithms=['HS256'])
print(json.dumps(payload, indent=2))
" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSJ9.sig"터미널 설정 없이 시각적으로 확인하고 싶다면 토큰을 ToolDeck JWT 디코더에 붙여넣으면 헤더, 페이로드, 서명 검증 상태를 즉시 확인할 수 있습니다.
python-jose 및 기타 대안
python-jose는 JWS, JWE(암호화된 토큰), JWK를 기본 지원하는 대안 JWT 라이브러리입니다. 페이로드 자체가 암호화된 JWT(JWE)를 처리해야 한다면 — PyJWT는 JWE를 전혀 지원하지 않기 때문에 — python-jose가 적합한 선택입니다. 이 라이브러리는 또한 내장 JWKS 키 셋 처리를 지원하여, 순환 키 셋을 노출하는 Auth0, Okta, Keycloak 같은 ID 공급자와의 통합이 간편합니다. 디코딩 인터페이스는 PyJWT와 거의 동일하여 전환에 필요한 코드 변경이 최소화됩니다:
# pip install python-jose[cryptography]
from jose import jwt as jose_jwt
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInNjb3BlIjoib3JkZXJzOnJlYWQifQ.signature"
# Verified decode — same pattern as PyJWT
payload = jose_jwt.decode(
token,
"signing-secret-key",
algorithms=["HS256"],
audience="https://api.example.com",
)
print(payload)
# {'sub': 'usr_8f2a', 'scope': 'orders:read'}
# Unverified decode
claims = jose_jwt.get_unverified_claims(token)
header = jose_jwt.get_unverified_header(token)
print(f"Algorithm: {header['alg']}, Subject: {claims['sub']}")제 추천: PyJWT로 시작하세요. JWT 사용 사례의 95%를 커버하고, 커뮤니티가 가장 크며, API도 깔끔합니다. JWE 지원이 필요하거나 JWKS 처리 방식이 더 마음에 든다면 python-jose로 전환하세요. 세 번째 옵션으로 Authlib도 언급할 만한데, 훨씬 큰 OAuth/OIDC 프레임워크 안에 JWT 처리를 번들로 제공합니다. 이미 OAuth 클라이언트 흐름에 Authlib를 사용하고 있다면 authlib.jose.jwt 모듈을 활용하면 JWT 의존성을 추가하지 않아도 됩니다. 그 외의 경우에는 토큰 디코딩만을 위한 무거운 의존성입니다.
터미널 출력에 문법 강조 적용
터미널에서 원시 JWT 클레임을 읽는 것은 빠른 확인에는 괜찮지만, 토큰 페이로드를 정기적으로 디버깅할 때(내부 인증 게이트웨이를 구축하면서 매일 이 작업을 했습니다), 색상이 적용된 출력은 실질적인 차이를 만들어 줍니다. 문자열, 숫자, 불리언, null이 각기 다른 색으로 렌더링되므로 모든 문자를 읽지 않고도 누락된 권한이나 잘못된 만료 타임스탬프를 한 눈에 발견할 수 있습니다.
rich 라이브러리(pip install rich)에는 print_json 함수가 있어 JSON 문자열이나 Python 딕셔너리를 받아 터미널에 완전한 문법 강조와 함께 출력합니다. PyJWT와 결합하면 두 줄짜리 JWT 점검 워크플로우를 만들 수 있습니다:
# pip install rich PyJWT
import jwt
from rich import print_json
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsInBlcm1pc3Npb25zIjpbIm9yZGVyczpyZWFkIiwicmVmdW5kczpjcmVhdGUiXSwiZXhwIjoxNzExODE1NjAwfQ.sig"
payload = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["HS256"]
)
# Colorized, indented JSON output in the terminal
print_json(data=payload)
# {
# "sub": "usr_8f2a", ← strings in green
# "role": "admin",
# "permissions": [
# "orders:read",
# "refunds:create"
# ],
# "exp": 1711815600 ← numbers in cyan
# }rich 출력에는 ANSI 이스케이프 코드가 포함됩니다. 파일에 쓰거나 API 엔드포인트에서 반환하지 마세요 — 터미널 표시 전용입니다. 일반 텍스트 출력이 필요할 때는 json.dumps()를 사용하세요.대용량 토큰 배치 처리
JWT 토큰 자체는 작지만(일반적으로 2KB 미만), 대량으로 처리해야 하는 상황이 있습니다. 보안 사고 후 감사 로그 분석. 인증 제공자 전환 시 세션 마이그레이션 스크립트. 지난 90일간 발급된 모든 토큰이 올바른 키로 서명되었음을 증명해야 하는 컴플라이언스 배치 검증. 수만 개의 토큰이 NDJSON 로그 파일에 있다면, 줄 단위 처리로 전체 파일을 메모리에 올리지 않고 점진적으로 결과를 보고할 수 있습니다.
감사 로그에서 토큰 배치 검증
import jwt
import json
from pathlib import Path
SECRET_KEY = "audit-log-signing-key"
def validate_token_log(log_path: str) -> dict:
"""Process an NDJSON file where each line has a 'token' field.
Returns counts of valid, expired, and invalid tokens.
"""
stats = {"valid": 0, "expired": 0, "invalid": 0}
with open(log_path) as fh:
for line_num, line in enumerate(fh, 1):
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
token = record["token"]
except (json.JSONDecodeError, KeyError):
stats["invalid"] += 1
continue
try:
jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
stats["valid"] += 1
except jwt.ExpiredSignatureError:
stats["expired"] += 1
except jwt.InvalidTokenError:
stats["invalid"] += 1
return stats
result = validate_token_log("auth-events-2026-03.ndjson")
print(f"Valid: {result['valid']}, Expired: {result['expired']}, Invalid: {result['invalid']}")
# Valid: 14832, Expired: 291, Invalid: 17NDJSON 토큰 내보내기에서 클레임 추출
import base64
import json
from datetime import datetime, timezone
def extract_claims_stream(input_path: str, output_path: str):
"""Read tokens line by line, decode payloads, write flattened claims."""
with open(input_path) as infile, open(output_path, "w") as outfile:
for line in infile:
line = line.strip()
if not line:
continue
record = json.loads(line)
token = record.get("access_token", "")
parts = token.split(".")
if len(parts) != 3:
continue
payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
# Flatten into an audit-friendly record
flat = {
"timestamp": record.get("timestamp"),
"subject": claims.get("sub"),
"issuer": claims.get("iss"),
"expired_at": datetime.fromtimestamp(
claims.get("exp", 0), tz=timezone.utc
).isoformat(),
}
outfile.write(json.dumps(flat) + "\n")
extract_claims_stream("token-audit.ndjson", "claims-extract.ndjson")multiprocessing.Pool을 사용하여 검증을 여러 코어에 분산하는 것을 고려하세요.흔한 실수
다음 네 가지 실수는 코드 리뷰와 Stack Overflow 질문에서 반복적으로 등장합니다. 각각 쉽게 발생하며, PyJWT가 제공하는 오류 메시지가 항상 원인을 직접 가리키지는 않습니다. 패키지 명명 문제 하나만으로도 누군가 잘못된 라이브러리를 설치하고 전혀 예상치 못한 API를 마주했을 때 수 시간의 디버깅 시간을 낭비하는 것을 보았습니다.
문제: pip install jwt 또는 pip install python-jwt를 실행하면 완전히 다른 패키지가 설치됩니다. 그러면 import jwt가 실패하거나 알 수 없는 API를 보게 됩니다.
해결: 항상 pip install PyJWT로 설치하세요. 임포트는 import jwt입니다. pip show PyJWT로 올바른 패키지가 설치되었는지 확인하세요.
pip install jwt # or pip install python-jwt import jwt # wrong package — different API entirely
pip install PyJWT import jwt # correct — this is PyJWT print(jwt.__version__) # 2.x
문제: PyJWT 1.x에서는 algorithms가 선택적이었고 모든 알고리즘을 허용하는 것이 기본값이었습니다. 이는 공격자가 alg: none을 설정한 토큰을 만들 수 있는 보안 취약점을 만들었습니다. PyJWT 2.x에서는 algorithms 없이 호출하면 DecodeError를 발생시킵니다.
해결: 항상 algorithms를 명시적인 목록으로 전달하세요. 애플리케이션이 실제로 발급하는 알고리즘만 사용하세요.
# PyJWT 2.x — this raises DecodeError payload = jwt.decode(token, SECRET_KEY) # DecodeError: algorithms must be specified
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
문제: algorithms=["RS256"]과 함께 문자열 시크릿을 jwt.decode()에 전달하면 InvalidSignatureError가 발생합니다. RS256은 공유 시크릿 문자열이 아닌 PEM 인코딩된 공개 키가 필요합니다.
해결: 파일 또는 환경 변수에서 PEM 공개 키를 불러오세요. cryptography 패키지를 설치하세요: pip install PyJWT cryptography.
# This fails — RS256 needs a public key, not a string secret payload = jwt.decode(token, "my-secret", algorithms=["RS256"]) # InvalidSignatureError
public_key = open("public_key.pem").read()
payload = jwt.decode(token, public_key, algorithms=["RS256"])문제: JWT base64url 인코딩은 끝의 = 문자를 제거합니다. Python의 base64.urlsafe_b64decode는 패딩을 수정하지 않고 원시 세그먼트를 전달하면 binascii.Error: Incorrect padding을 발생시킵니다.
해결: 디코딩 전에 패딩을 추가하세요: segment += '=' * (-len(segment) % 4). 이 공식은 항상 올바른 수의 패딩 문자(0, 1, 2, 또는 3)를 생성합니다.
import base64
payload_b64 = token.split(".")[1]
data = base64.urlsafe_b64decode(payload_b64)
# binascii.Error: Incorrect paddingimport base64
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4)
data = base64.urlsafe_b64decode(payload_b64) # worksPyJWT vs 대안 — 빠른 비교
PyJWT는 대부분의 Python 애플리케이션을 위한 올바른 출발점입니다. HMAC과(cryptography 백엔드를 사용하면) RSA 및 EC 서명 검증을 커버합니다. JWE(암호화된 토큰)가 필요하다면 python-jose 또는 Authlib로 전환하세요. 수동 base64 디코딩은 디버깅에는 작동하지만 안전성 보장이 전혀 없습니다.
각 옵션을 언제 선택하는지: 표준 HS256 또는 RS256 검증을 수행하는 웹 서비스에는 PyJWT. 아키텍처에 암호화된 토큰이나 JWKS 순환이 포함되면 python-jose. pip를 사용할 수 없는 환경(CI 컨테이너, 제한된 프로덕션 호스트, 의존성을 최소화하고 싶은 AWS Lambda 콜드 스타트)에서 빠른 점검에는 수동 base64. 프로젝트에서 이미 OAuth 클라이언트 흐름에 Authlib를 사용하고 있고 별도의 JWT 라이브러리 추가가 중복이 될 때는 Authlib.
코드 없이 확인하려면 토큰을 JWT 디코더에 붙여넣으면 디코딩된 헤더와 페이로드를 클레임 검증 피드백과 함께 볼 수 있습니다.
자주 묻는 질문
Python에서 서명 검증 없이 JWT를 디코딩하려면 어떻게 하나요?
jwt.decode()에 options={"verify_signature": False}와 algorithms=["HS256"]을 전달하면 됩니다. 서명을 확인하지 않고 페이로드 딕셔너리를 반환합니다. 올바른 공개 키를 가져오기 전 클레임을 읽거나 개발 중에 디버깅할 때만 사용하세요. 접근 제어에 사용되는 토큰에 대해서는 검증을 건너뛰지 마세요.
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
payload = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["HS256"]
)
print(payload)
# {'sub': 'usr_8f2a', 'role': 'admin'}PyJWT와 python-jwt의 차이점은 무엇인가요?
PyJWT(pip install PyJWT, import jwt)는 월 8천만 건 이상의 다운로드를 기록하는 Python 가장 인기 있는 JWT 라이브러리입니다. python-jwt(pip install python_jwt)는 별도의 라이브러리로 사용자 수가 훨씬 적고 API 인터페이스도 다릅니다. 코드에서 import jwt를 보면 PyJWT를 사용하는 것입니다. PyPI 패키지 이름(PyJWT)과 임포트 이름(jwt)이 다르기 때문에 혼동이 생기는 것입니다. 특별한 이유가 없다면 PyJWT를 사용하세요.
Python에서 RS256 JWT를 어떻게 디코딩하나요?
PyJWT와 cryptography 백엔드를 함께 설치하세요: pip install PyJWT cryptography. 그런 다음 PEM 인코딩된 공개 키를 key 인수로 전달하고 algorithms=["RS256"]을 지정합니다. PyJWT는 RSA 서명 검증을 cryptography 라이브러리에 위임합니다. cryptography 패키지 없이는 RSA 또는 EC 알고리즘 사용 시 PyJWT가 오류를 발생시킵니다.
import jwt
public_key = open("public_key.pem").read()
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://api.example.com"
)PyJWT에서 ExpiredSignatureError가 발생하는 이유는 무엇인가요?
PyJWT는 기본적으로 exp(만료) 클레임을 확인합니다. 현재 UTC 시각이 exp 타임스탬프를 지났다면 jwt.ExpiredSignatureError를 발생시킵니다. leeway 파라미터로 클럭 오차 허용값을 추가할 수 있습니다: jwt.decode(token, key, algorithms=["HS256"], leeway=timedelta(seconds=30)). 이렇게 하면 30초의 유예 기간이 생깁니다. 만료 검사를 완전히 비활성화하려면(프로덕션에는 권장하지 않음) options={"verify_exp": False}를 전달하세요.
import jwt
from datetime import timedelta
try:
payload = jwt.decode(token, secret, algorithms=["HS256"], leeway=timedelta(seconds=30))
except jwt.ExpiredSignatureError:
print("Token has expired — prompt re-authentication")Python에서 라이브러리 없이 JWT 클레임을 읽을 수 있나요?
가능합니다. 토큰을 점(.)으로 나누고 두 번째 세그먼트(페이로드)를 가져와서 길이가 4의 배수가 되도록 = 문자를 패딩한 뒤 base64url 디코딩 후 JSON을 파싱하면 됩니다. 서명은 검증되지 않지만 PyJWT를 설치할 수 없는 제한된 환경이나 빠른 디버깅 스크립트에 유용합니다.
import base64, json
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSJ9.signature"
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4) # fix padding
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
print(claims) # {'sub': 'usr_8f2a'}PyJWT에서 audience 클레임을 어떻게 검증하나요?
jwt.decode()에 audience 파라미터를 전달하면 됩니다: jwt.decode(token, key, algorithms=["HS256"], audience="https://api.example.com"). PyJWT는 토큰의 aud 클레임과 제공한 값을 비교합니다. 토큰에 aud 클레임이 없거나 값이 일치하지 않으면 jwt.InvalidAudienceError를 발생시킵니다. 여러 API를 위한 토큰을 허용해야 하는 경우 허용 가능한 audience 목록을 전달할 수도 있습니다.
import jwt
payload = jwt.decode(
token,
secret,
algorithms=["HS256"],
audience=["https://api.example.com", "https://admin.example.com"]
)관련 도구
Dmitri is a DevOps engineer who relies on Python as his primary scripting and automation language. He builds internal tooling, CI/CD pipelines, and infrastructure automation scripts that run in production across distributed teams. He writes about the Python standard library, subprocess management, file processing, encoding utilities, and the practical shell-adjacent Python that DevOps engineers use every day.
Maria is a backend developer specialising in Python and API integration. She has broad experience with data pipelines, serialisation formats, and building reliable server-side services. She is an active member of the Python community and enjoys writing practical, example-driven guides that help developers solve real problems without unnecessary theory.