본문으로 건너뛰기
Toova
모든 도구

JWT란 무엇이며 안전하게 디코딩하는 방법

Toova

JSON Web Token은 현대 웹 개발 어디에나 나타납니다 — 인증 헤더, OAuth 흐름, API 키, 세션 관리. Bearer 토큰이 필요한 API로 작업했다면 이미 JWT를 사용한 것입니다. 그러나 대부분의 개발자는 그것들을 불투명한 블롭으로 취급하고 거의 안을 보지 않습니다. JWT의 구조, 각 부분이 무엇을 의미하는지, 그리고 보안 실수 없이 디코딩하는 방법을 이해하는 데 약 15분이 걸립니다. 이 가이드는 모든 것을 다룹니다.

JWT의 해부학

모든 JWT는 점으로 구분된 세 개의 Base64URL 인코딩 세그먼트 문자열입니다:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzQ3MDAwMDAwLCJpYXQiOjE3NDY5OTY0MDAsInNjb3BlIjoicmVhZDp1c2VycyJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

형식은 header.payload.signature입니다. 각 세그먼트는 다른 정보를 인코딩합니다.

헤더

첫 번째 세그먼트는 헤더입니다. 위 예제를 디코딩하면 다음과 같습니다:

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

alg는 토큰에 서명하는 데 사용된 알고리즘을 지정합니다 — RS256은 SHA-256이 있는 RSA를 의미합니다. typ은 토큰 타입을 식별합니다. 이 두 필드가 최소이며, 일부 토큰은 여러 키가 순환 중일 때 검증자가 어떤 공개 키를 사용해야 하는지 알리기 위해 kid(키 ID)도 포함합니다.

페이로드

두 번째 세그먼트는 페이로드 — 실제 데이터입니다. 디코딩하면:

{
  "sub": "user_123",
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "exp": 1747000000,
  "iat": 1746996400,
  "scope": "read:users"
}

페이로드에는 클레임이 포함됩니다: 토큰이 나타내는 엔티티에 대한 진술. 일부 클레임은 표준화되어 있고(등록된 클레임이라고 함), 다른 클레임은 애플리케이션 특화입니다(비공개 클레임이라고 함). 페이로드는 암호화되지 않고 Base64URL 인코딩됩니다. 토큰 문자열을 얻는 누구나 그 안의 모든 클레임을 읽을 수 있습니다.

서명

세 번째 세그먼트는 서명입니다. 인코딩된 헤더와 페이로드를 가져와 점으로 연결한 다음 alg에 지정된 시크릿 키나 비공개 키로 결과에 서명하여 생성됩니다:

signature = sign(
  base64url(header) + "." + base64url(payload),
  secretOrPrivateKey
)

서명은 해당 공개 키(또는 대칭 알고리즘의 경우 시크릿 키)를 가진 모든 당사자가 토큰이 신뢰할 수 있는 출처에서 발급되었고 헤더나 페이로드가 발급 후 수정되지 않았음을 검증할 수 있게 합니다. 페이로드의 단일 변경된 문자는 서명을 완전히 무효화합니다.

JWT를 디코딩하는 방법

디코딩은 검증과 다릅니다. 디코딩은 단순히 토큰 내부의 데이터를 읽습니다. 검증은 토큰이 진본이고 만료되지 않았음을 확인합니다. 프로덕션 코드에서는 항상 검증해야 합니다; 디코딩만으로는 디버깅에 유용합니다.

1단계: 토큰 분할

JWT 문자열을 . 문자로 분할합니다. 세 개의 세그먼트를 얻습니다. 세 개 미만이면 토큰이 잘못된 형식입니다.

2단계: 헤더와 페이로드 디코딩

처음 두 세그먼트 각각을 Base64URL 디코딩합니다. Base64URL은 표준 Base64와 비슷하지만 + 대신 -를 사용하고 / 대신 _를 사용하며 패딩 문자가 없습니다. 디코딩되면 각 세그먼트를 JSON으로 파싱할 수 있습니다.

3단계: 파싱 및 검사

클레임을 읽습니다. 현재 Unix 타임스탬프에 대해 exp(만료)를 확인합니다. 토큰이 애플리케이션을 위한 것인지 확인하기 위해 issaud를 살펴봅니다. 서명을 검증할 때까지 어떤 클레임도 신뢰하지 마세요.

개발 중 빠른 검사를 위해 JWT Decoder를 사용하세요. 브라우저에서 완전히 실행됩니다 — 토큰이 기기를 떠나지 않습니다. 원시 바이트를 보려면 Base64 인코더/디코더로 base64 세그먼트를 수동으로 디코딩할 수도 있습니다. 테스트 스크립트에서 서명을 검증하려면 HMAC 생성기로 코드를 작성하지 않고 HS256 서명을 재현할 수 있습니다.

4단계: 코드에서 서명 검증

프로덕션에서는 항상 언어용으로 구축된 라이브러리를 사용하여 서명을 검증하세요. 서명 검증을 손으로 구현하지 마세요. 인기 있는 옵션에는 Node.js용 josejsonwebtoken, Python용 PyJWT, Go용 golang-jwt/jwt, Java용 nimbus-jose-jwt가 포함됩니다.

항상 알고리즘을 명시적으로 전달하세요. 라이브러리가 토큰 헤더에서 알고리즘을 추론하도록 절대 두지 마세요.

표준 클레임(등록된 클레임)

RFC 7519는 등록된 클레임 이름 세트를 정의합니다. 이것들은 필수가 아니지만 존재할 때 표준 의미를 따라야 합니다.

iss — 발급자

토큰을 만들고 서명한 엔티티. 일반적으로 권한 부여 서버를 식별하는 URL(예: https://auth.example.com). 검증자는 iss가 예상 값과 일치하는지 확인하고 알 수 없는 발급자의 토큰을 거부해야 합니다.

sub — 주제

토큰이 나타내는 주체 — 일반적으로 사용자 ID, 서비스 계정 이름 또는 기기 식별자. 값은 발급자 컨텍스트 내에서 고유해야 합니다. 애플리케이션은 요청이 어떤 사용자에 속하는지 식별하기 위해 sub를 사용합니다.

aud — 대상

토큰의 의도된 수신자(들). API의 식별자가 https://api.example.com이라면 다른 서비스를 위해 발급된 토큰은 거부되어야 합니다. aud를 검증하지 않으면 공격자가 한 서비스에서 얻은 토큰을 다른 서비스에서 재사용할 수 있습니다.

exp — 만료 시간

그 이후로 토큰이 수락되어서는 안 되는 Unix 타임스탬프. 항상 exp를 검증하세요. 만료가 없는 토큰은 사실상 영원히 살아 있습니다 — 도난당하면 공격자는 무기한 액세스 권한을 가집니다.

iat — 발급 시간

토큰이 만들어진 Unix 타임스탬프. 기술적으로 만료되지 않았지만 의심스럽게 오래된 토큰을 감지하는 데 유용합니다. iat를 사용하여 exp와 독립적으로 최대 토큰 나이를 시행할 수 있습니다.

nbf — Not Before

그 이전에 토큰이 수락되어서는 안 되는 Unix 타임스탬프. exp보다 덜 일반적이지만 토큰을 미리 발급할 때 유용합니다 — 예를 들어 특정 시간까지 시작해서는 안 되는 예약된 작업.

jti — JWT ID

토큰의 고유 식별자. 발급자가 사용된 jti 값을 저장하고 중복 ID가 있는 토큰을 거부하여 토큰 재생 공격을 방지할 수 있게 합니다. 비밀번호 재설정 링크 같은 일회성 사용 토큰이 필요할 때 필수입니다.

보안 함정

JWT 구현에는 미묘한 취약점의 역사가 있습니다. 이것들은 JWT를 받는 코드를 출시하기 전에 이해해야 할 가장 중요한 것입니다.

"alg: none" 공격

표준 초기의 일부 JWT 라이브러리는 헤더에 "alg": "none"이 지정된 토큰을 받아들였습니다. 공격자가 모든 유효한 토큰을 가져와 알고리즘을 none으로 대체하고 서명을 제거하면 라이브러리가 완전히 유효한 것으로 받아들였습니다 — 서명 필요 없음.

수정: 항상 검증 함수를 호출할 때 예상 알고리즘을 명시적으로 지정하세요. alg: none을 주장하는 모든 토큰을 잘못된 것으로 취급하세요. 대부분의 현대 라이브러리가 이를 해결했지만 프로덕션으로 가기 전에 라이브러리 기본값을 확인할 가치가 있습니다.

알고리즘 혼동(RS256에서 HS256으로 다운그레이드)

두 알고리즘을 모두 지원하는 일부 라이브러리는 검증 방법을 결정하기 위해 토큰 헤더의 alg 필드를 사용했습니다. 공격자가 알고리즘을 HS256으로 변경한 다음 서버의 공개 키를 HMAC 시크릿으로 사용하여 토큰에 서명할 수 있었습니다. HS256을 보는 서버는 공개 키에 대해 검증하고 위조된 토큰을 받아들였습니다.

수정은 동일합니다: 검증 코드에서 알고리즘을 잠그세요. 토큰 헤더가 검증에 사용되는 알고리즘에 영향을 주도록 절대 두지 마세요.

만료된 토큰 수락

exp를 검증하지 않는 것은 일반적인 간과입니다 — 때때로 개발자가 무제한으로 자라는 유예 기간을 추가하거나 코드 분기에서 검증 코드 경로가 우회될 때 도입됩니다. 만료된 토큰을 누락된 토큰과 동일하게 취급하세요: 401로 거부하고 클라이언트가 다시 인증하거나 리프레시 토큰을 사용하도록 요구하세요.

페이로드의 민감한 데이터

페이로드는 암호화되지 않고 Base64URL 인코딩됩니다. 토큰을 가로채는 누구나 그것을 디코딩할 수 있습니다. 비밀번호, 신용카드 번호, 사회 보장 번호 또는 기타 민감한 데이터를 페이로드에 넣지 마세요. 민감한 클레임을 안전하게 전송해야 한다면 페이로드를 암호화하는 JWE(JSON Web Encryption) 토큰을 사용하세요. JWS(일반적인 경우)로 서명된 JWT는 기밀성이 아닌 진위성만 보장합니다.

대상 검증 누락

aud 검증을 건너뛰는 것은 서비스 A를 위해 발급된 토큰이 서비스 B에서 재생될 수 있음을 의미합니다 — 두 서비스가 동일한 서명 키를 공유하거나 동일한 발급자를 신뢰하는 한. 다중 서비스 아키텍처에서 항상 토큰의 대상이 서비스의 식별자와 일치하는지 검증하세요.

2026년 모범 사례

대부분의 애플리케이션에 RS256 선택

RS256은 서명에 비공개 키를 사용하고 검증에 공개 키를 사용합니다. 권한 부여 서버만이 비공개 키를 보유합니다. 모든 서비스가 공개 키를 사용하여 토큰을 검증할 수 있으며, 공개 키는 (종종 JWKS 엔드포인트를 통해) 공개적으로 게시될 수 있습니다. 개별 서비스가 손상되면 공격자는 토큰을 검증할 능력을 얻습니다 — 그러나 새 것을 위조할 수는 없습니다.

HS256은 서명과 검증 모두에 단일 공유 시크릿을 사용합니다. 토큰을 검증할 수 있는 모든 서비스는 또한 그것들을 만들 수 있습니다. 마이크로서비스 설정에서 많은 서비스에 시크릿을 공유하면 침해의 폭발 반경이 증가합니다.

액세스 토큰을 단기로 유지

JWT에 대한 내장 폐기 메커니즘이 없습니다 — 발급되면 토큰은 만료될 때까지 유효합니다(차단 목록을 구현하지 않는 한, 그것은 서버 측 상태를 다시 도입합니다). 짧은 만료 시간(15분에서 1시간)은 토큰이 도난당한 경우 손상 창을 제한합니다. 액세스 토큰을 localStorage가 아닌 안전한 HttpOnly 쿠키에 저장된 장기 리프레시 토큰과 짝지으세요.

리프레시 토큰 순환 사용

클라이언트가 리프레시 토큰을 사용하여 새 액세스 토큰을 얻을 때 새 리프레시 토큰을 발급하고 이전 것을 무효화하세요. 이렇게 하면 도난당한 리프레시 토큰은 합법적인 클라이언트가 다음에 원본을 사용하려고 할 때 감지됩니다: 서버는 재사용된 리프레시 토큰을 보고 전체 세션을 폐기할 수 있습니다. 이 패턴은 RFC 6819에 설명되어 있으며 현대 권한 부여 서버에서 광범위하게 지원됩니다.

JWKS 엔드포인트 게시

JSON Web Key Set(JWKS) 엔드포인트(관례로 /.well-known/jwks.json)는 표준 형식으로 서명 공개 키를 게시합니다. 다른 서비스는 키를 외부 채널로 받지 않고 JWKS를 가져와 토큰을 검증할 수 있습니다. 이는 또한 키 순환을 간단하게 만듭니다: 새 키를 JWKS에 추가하고, 그것으로 서명하기 시작한 다음, 모든 기존 토큰이 만료되면 이전 키를 제거합니다.

주기적으로 키 순환

비공개 키가 절대 손상되지 않더라도 주기적으로 키를 순환하면 도난당한 키가 토큰을 위조하는 데 사용될 수 있는 창이 제한됩니다. JWT 헤더에 kid를 사용하여 서명 키를 참조하면 검증자가 순환 중 기존 토큰을 깨뜨리지 않고 JWKS에서 올바른 공개 키를 선택할 수 있습니다.

모든 클레임 검증

모든 요청에서 iss, aud, expnbf를 확인하세요. 설정에서 중복되어 보이기 때문에 이것들 중 어느 것도 건너뛰지 마세요 — 그것들이 오늘 불필요해 보이게 만드는 조건이 정확히 사건 중에 변하는 조건입니다.

JWT 대 세션 쿠키

JWT와 서버 측 세션은 동일한 문제를 해결합니다 — 무상태 HTTP 전반에 걸친 인증 상태 유지 — 그러나 다른 트레이드오프가 있습니다.

JWT는 자체 포함입니다. 서버는 요청을 검증하기 위해 데이터베이스를 쿼리할 필요가 없습니다. 이는 분산 시스템과 모바일이나 서드파티 클라이언트가 소비하는 API에 매력적으로 만듭니다. 단점은 폐기에는 차단 목록(서버 측 상태를 다시 도입)이나 손상된 토큰의 남은 수명을 견디는 것이 필요하다는 것입니다.

세션 쿠키는 서버 검증됩니다. 서버는 세션 상태를 저장하고 세션을 삭제하여 즉시 액세스를 폐기할 수 있습니다. HttpOnlySecure 플래그가 있는 쿠키는 JavaScript 액세스와 네트워크 가로채기로부터 보호됩니다. 단점은 모든 요청에 데이터베이스 조회가 필요하다는 것이며, 이는 규모에서 병목이 될 수 있습니다.

서버 렌더링 페이지와 단일 백엔드가 있는 전통적인 웹 애플리케이션의 경우 세션 쿠키는 견고하고 더 간단한 선택으로 남아 있습니다. 여러 클라이언트, 마이크로서비스 아키텍처 및 모바일 앱이 소비하는 API의 경우 짧은 만료와 리프레시 토큰 순환이 있는 JWT가 더 실용적인 옵션입니다.

토큰 디코딩

JWT를 검사하는 가장 빠른 방법은 JWT Decoder에 붙여넣는 것입니다. 토큰을 분할하고, 헤더와 페이로드를 디코딩하며, 클레임을 읽을 수 있는 형식으로 렌더링합니다 — 모두 토큰을 어디로도 보내지 않고. 원시 Base64URL 문자열을 수동으로 인코딩하거나 디코딩해야 한다면 Base64 인코더/디코더가 표준과 URL 안전 변형 모두를 처리합니다. 테스트 스크립트에서 HMAC 서명을 검증하려면 HMAC 생성기로 HS256 서명을 재현하고 토큰이 포함하는 것과 비교할 수 있습니다.

전체 명세는 RFC 7519(JWT)와 jwt.io의 대화형 예제를 참조하세요.