← 전체 글

JWT 디코딩하고 클레임 읽는 방법

JWT는 세 개의 Base64url 섹션. JavaScript와 Python에서 헤더와 페이로드를 디코딩하는 법 —— 그리고 토큰 디코딩이 검증과 같지 않은 이유.

JWT(JSON Web Token)는 점으로 연결된 세 개의 Base64url 인코딩된 섹션입니다. 클레임을 읽기 위해 하나를 디코드하는 데는 한 번의 함수 호출이면 되고 비밀 키가 필요 없습니다 —— 바로 그래서 JWT 페이로드는 민감 데이터를 둘 안전한 곳이 아닙니다. 본 가이드는 JWT 가 무엇인지, 각 부분을 어떻게 디코드하는지, 그리고 디코딩검증의 결정적 차이를 설명합니다.

JWT 란?

JWT 는 당사자 간에 클레임 —— 보통 사용자의 신원과 권한 —— 을 운반하는 데 쓰이는 콤팩트하고 URL 안전한 토큰입니다. RFC 7519 로 표준화되었습니다. 토큰은 이렇게 생겼습니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

두 점이 세 부분으로 나눕니다: header, payload, signature.

세 부분

  • Header —— 서명 알고리즘을 기술하는 작은 JSON 객체. 예: {"alg":"HS256","typ":"JWT"}.
  • Payload —— claims 의 JSON 객체. 예: sub(subject), exp(만료), 사용자 정의 필드.
  • Signature —— header 와 payload 에 대한 암호 서명으로 변조 감지를 위한 것. 사람이 읽을 수 없고 JSON 도 아닙니다.

header 와 payload 는 Base64url 인코딩된 JSON 입니다. 서명은 JWS(RFC 7515)로 만들어집니다.

JavaScript 에서 JWT 디코드

점으로 분할하고 처음 두 부분을 Base64url 디코드. JWT 는 Base64url(+/ 대신 -_, 패딩 없음)을 쓰므로 디코드 전 변환이 필요합니다:

function decodeJwt(token) {
  const [headerB64, payloadB64] = token.split('.');

  const decode = (b64url) => {
    const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
    const json = atob(b64);
    return JSON.parse(json);
  };

  return { header: decode(headerB64), payload: decode(payloadB64) };
}

완전한 Unicode 안전을 위해 Base64 를 바이트로 디코드한 뒤 TextDecoder 를 통과시키세요. atob 만 쓰지 마세요.

Python 에서 JWT 디코드

import base64, json

def decode_jwt(token):
    header_b64, payload_b64, _sig = token.split('.')
    def decode(part):
        padded = part + '=' * (-len(part) % 4)
        return json.loads(base64.urlsafe_b64decode(padded))
    return decode(header_b64), decode(payload_b64)

디코딩은 검증이 아닙니다

JWT 의 가장 중요한 점입니다. 누구나 토큰을 디코드하고 페이로드를 읽을 수 있습니다 —— 키 불필요. 디코딩은 토큰이 무엇을 주장하는지 만 알려주며 그 주장이 신뢰할 만한지는 알려주지 않습니다.

토큰을 신뢰하려면 검증된 라이브러리를 써서 서버의 비밀 키나 공개 키에 대해 서명을 검증 해야 합니다 —— 직접 만들지 마세요:

import jwt from 'jsonwebtoken';

// 서명 검증 AND 만료 확인; 무효면 던짐
const claims = jwt.verify(token, process.env.JWT_SECRET);

// jwt.decode(token) —— 검증하지 않음; 검토용으로만

서버 검증이 JWT 를 자격 증명으로 만듭니다. 디코드된 페이로드를 검증 없이 신뢰하는 것은 고전적인 인증 버그입니다.

JWT 페이로드는 암호화되지 않습니다

페이로드는 단지 Base64url 인코딩된 JSON 이므로 기밀성을 전혀 제공하지 않습니다. 이것은 Base64 는 암호화가 아님 과 같은 오해입니다: 인코딩은 누구나 되돌릴 수 있습니다.

비밀번호, 전체 신용카드 번호, 민감한 개인정보를 JWT 페이로드에 넣지 마세요. 진짜 암호화된 토큰이 필요하면 JWE(JSON Web Encryption, RFC 7516)를 쓰세요 —— 별개의 훨씬 덜 흔한 표준. 실제 토큰을 신뢰할 수 없는 온라인 디코더에 붙여 넣는 것도 피하세요; 민감한 JSON 을 온라인 도구에 붙여 넣지 말아야 하는 이유.

검증 순서: 서명 먼저, 그다음 클레임

프로덕션에서 토큰을 검증할 때 순서가 중요합니다. 안전한 체크리스트:

  1. 알고리즘 허용 목록 —— 기대 집합(예: ['RS256'])에 없는 것은 거부. none 을 절대 수락 금지.
  2. 서명 —— 올바른 키로 검증(JWKS 의 kid 로 조회).
  3. exp —— 토큰이 만료되지 않았어야 함.
  4. nbf(not-before) —— 너무 일찍 사용되지 않았어야 함.
  5. iat —— 발급 시간이 너무 먼 미래가 아닌지 정합성 확인(시계 편차 제외).
  6. iss —— 발급자가 기대하는 신뢰 권위여야 함.
  7. aud —— audience 가 당신의 서비스를 포함해야 함.

대부분의 JWT 라이브러리는 1-4 를 기본 수행하고 6-7 은 옵트인이 필요합니다. issaud 가 확인되지 않으면, 생태계의 다른 서비스용 유효 토큰이 당신의 서비스에 재생될 수 있습니다.

표준 클레임

Claim의미
issIssuer —— 토큰을 만든 자
subSubject —— 토큰의 대상(보통 사용자 ID)
audAudience —— 토큰의 수신처
expExpiry —— 이 Unix 타임스탬프 이후 무효
iatIssued-at —— 토큰 발급 시 Unix 타임스탬프
nbfNot-before —— 이 시간 이전에는 무효

kid 헤더와 alg: none 함정

실제 JWT 에 자주 나타나는 두 헤더 필드, 한눈에 알아볼 수 있어야 합니다:

  • kid(key ID) —— 키 집합(JWKS)의 어느 키가 토큰을 서명했는지 검증기에 알려주는 힌트. 검증기가 서명 확인 전에 올바른 키를 고를 수 있도록 헤더에 있습니다. kid 를 신뢰할 수 없는 입력으로 취급: 고정 허용 목록(또는 가져온 JWKS)에서 조회하고 파일 경로, SQL 매개변수, URL 로 쓰지 마세요.
  • alg: "none" —— 합법이지만 역사적으로 위험한 알고리즘으로 "서명 없음" 을 뜻합니다. 디코더는 즐겁게 디코드하고 관대한 검증기는 받아들여 위조 페이로드를 "유효" 토큰으로 만들 수 있습니다. alg: none 을 명시적으로 거부 하고 라이브러리를 기대하는 정확한 알고리즘(예: ['RS256'])으로 설정해 토큰 자체 헤더의 주장 알고리즘을 믿지 마세요.

브라우저에서 JWT 디코드

토큰의 클레임을 빠르게 검사하려면 임의의 Base64url 섹션을 fixjson 의 Base64 디코더 에 붙여 넣으세요 —— 로컬에서 JSON 으로 디코드하고 서버로 아무것도 보내지 않습니다. 민감하지 않은 토큰에는 전용 디버거를 써도 되지만 실제 사용자 데이터가 포함된 것에는 로컬 우선 도구가 안전한 선택입니다.

자주 묻는 질문

JWT 를 어떻게 디코드하나요?

토큰을 점으로 분할하고 처음 두 부분(header 와 payload)을 Base64url 디코드해 JSON 으로 만드세요. 세 번째는 서명이고 사람이 읽을 수 없습니다. 디코딩에 키가 필요 없습니다.

JWT 디코딩과 검증이 같은 건가요?

아니요. 디코딩은 클레임을 읽을 뿐이고 누구나 할 수 있습니다. 검증은 비밀 키나 공개 키로 서명을 확인해 토큰이 진짜이고 변조되지 않았음을 확인합니다. 토큰을 신뢰하기 전 항상 서버에서 검증하세요.

JWT 의 데이터를 누구나 읽을 수 있나요?

예. 페이로드는 Base64url 인코딩이고 암호화가 아닙니다 —— 토큰을 가진 사람에게는 평문입니다. JWT 페이로드에 비밀을 저장하지 마세요. Base64 는 암호화가 아님.

JWT, JWS, JWE 의 차이는?

서명된 JWT 는 JWS(RFC 7515)를 씁니다 —— 페이로드 읽기 가능, 변조 감지. JWE(RFC 7516)는 페이로드를 실제 암호화해 기밀성을 제공. 일상에서 보는 보통 JWT 는 JWS 입니다.

kid 클레임은 무엇에 쓰나요?

kid 는 헤더 필드(페이로드 클레임이 아님)로 JWKS 의 어느 키가 토큰을 서명했는지 식별해 검증기가 올바른 키를 고르게 합니다. 당신이 통제하는 고정 키 집합에서 조회하세요 —— 파일 경로나 URL 로 신뢰하지 마세요.

alg: "none" 이 위험한가요?

"서명 없음" 을 뜻합니다. 토큰 자체의 알고리즘 필드를 존중하는 관대한 검증기는 위조 페이로드를 받아들입니다. 항상 라이브러리를 기대하는 정확한 알고리즘(예: ['RS256'])으로 설정하고 none 을 명시적으로 거부하세요.

로컬에서 토큰 검사