JSON Web Token은 서명된 JSON 클레임을 담아 서버가 저장된 세션 없이도 요청을 검증할 수 있게 하는, 간결하고 URL 안전한 토큰입니다. 이는 RFC 7519 자체의 표현입니다. “두 당사자 사이에 전달될 클레임을 표현하는 간결하고 URL 안전한 수단”이라는 문장 속 두 가지 개념이 이후의 모든 것을 떠받칩니다. 토큰이 자신의 사실을 담고 있다는 것, 그리고 서명 덕분에 올바른 키를 가진 누구든 데이터베이스를 거치지 않고 그 사실을 검사할 수 있다는 것입니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 형태 | header.payload.signature – 점으로 이어진 세 개의 Base64Url 부분 |
| 핵심 원리 | 누구나 디코딩할 수 있고, 키 소지자만 위조할 수 있음 |
| 암호화가 아님 | 페이로드는 읽을 수 있음 – 서명됨 ≠ 비밀(그것은 JWE의 역할) |
| 트레이드오프 | 무상태 검증 ↔ exp까지 내장된 폐기 수단 없음 |
| 규율 | 알고리즘 고정 · iss/aud/exp 검증 · 짧은 수명 + 리프레시 토큰 회전 |
JWT 디코딩
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiJ1c3ItOGZrMiIsInJvbGUi… . dBjftJeZ4CVP…
header { "alg": "HS256", "typ": "JWT" }
payload { "sub": "usr-8fk2", "role": "editor",
"iss": "https://api.example.com", "aud": "example-web",
"iat": 1767024900, "exp": 1767025800 } ← 15분 수명
signature HMACSHA256( base64url(header) + "." + base64url(payload), secret )
앞의 두 부분은 누구나 디코딩할 수 있습니다 – Base64Url은 포장이지 암호화가 아닙니다.
세 번째 부분은 키 소지자만 위조할 수 있습니다 – 이것이 원리의 전부입니다.
코드로 볼 만한 대비, 즉 모든 JWT 결정의 기준이 되는 상태 유지 대안입니다:
// JavaScript / Node.js — Back4app JS SDK
// The stateful contrast: Back4app issues revocable session tokens
const user = await Parse.User.logIn('ada', 'correct-horse-battery');
const token = user.getSessionToken(); // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
await Parse.User.logOut(); // token invalid NOW — no waiting for an exp claim // Flutter / Dart — Back4app Flutter SDK
// The stateful contrast: Back4app issues revocable session tokens
final user = ParseUser('ada', 'correct-horse-battery', null);
await user.login();
final token = user.sessionToken; // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
await user.logout(); // token invalid NOW — no waiting for an exp claim // iOS / Swift — Back4app Swift SDK
// The stateful contrast: Back4app issues revocable session tokens
let user = try await User.login(username: "ada", password: "correct-horse-battery")
let token = user.sessionToken // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
try await User.logout() // token invalid NOW — no waiting for an exp claim // Android / Kotlin — Back4app Android SDK
// The stateful contrast: Back4app issues revocable session tokens
val user = ParseUser.logIn("ada", "correct-horse-battery")
val token = user.sessionToken // r:abc123… — opaque, server-side state
// Every request presents it; the server can revoke it instantly:
ParseUser.logOut() // token invalid NOW — no waiting for an exp claim 검증의 동작 방식
설명 글들이 건너뛰는 정밀함이 있습니다. “JWT”는 클레임 형식의 이름이고, 실제로 모두가 주고받는 것은 JWS(RFC 7515), 즉 서명된 직렬화입니다. JWE는 읽히지 않아야 하는 페이로드를 위한 암호화 형제입니다. 그리고 검증은 한 가지가 아니라 두 가지 일입니다. 서명을 검사한 뒤 클레임을 확인해야 합니다. exp와 nbf는 시계와, iss는 신뢰하는 발급자의 허용 목록과, aud는 이 서비스의 식별자와 대조합니다. 완벽하게 서명되었지만 만료되었거나, 잘못된 발급자가 발급했거나, 다른 대상을 위해 발급된 토큰은 완벽하게 서명된 공격입니다.
클레임: 페이로드의 어휘
| 클레임 | 이름 | 검증자의 역할 |
|---|---|---|
iss | 발급자(Issuer) | 내가 신뢰하는 발급자인가? |
sub | 주체(Subject) | 누구에 관한 토큰인가 – 안정적인 사용자 ID |
aud | 대상(Audience) | 나를 위해 발급되었는가? 다른 서비스의 토큰은 거부 |
exp | 만료(Expiration) | 이 Unix 타임스탬프 이후에는 거부 |
iat / nbf | 발급 시각 / 유효 시작 | 유효 기간의 타당성 검사 |
jti | 토큰 ID | 고유 핸들 – 거부 목록에 필요한 고리 |
| 커스텀 | 역할, 테넌트, 요금제… | 앱 의미론 – 최소한으로 유지, 비밀은 절대 금지 |
페이로드는 두 가지 이유로 가볍게 유지해야 합니다. 토큰은 모든 요청에 헤더로 실려 다니고(쿠키는 4 KB 근처에서 한계에 이르며, 클레임 하나하나가 반복되는 대역폭입니다), 그 안의 모든 것은 토큰을 가진 누구에게나 읽힙니다.
JWT vs. 세션 토큰
| JWT(무상태) | 세션 토큰(상태 유지) | |
|---|---|---|
| 토큰의 정체 | 서명된 상태 그 자체 | 서버 상태를 가리키는 불투명한 포인터 |
| 요청당 비용 | 서명 검사, 조회 없음 | 세션 저장소 조회 한 번 |
| 폐기 | exp까지 불가 – 설계상 그러함 | 즉시 – 세션 삭제 |
| 서비스 간 인증 | 키를 가진 어떤 서비스든 검증 | 서비스들이 세션 저장소를 공유해야 함 |
| 로그아웃의 의미 | 클라이언트가 버림, 토큰은 여전히 유효 | 토큰이 실제로 소멸 |
| 최적의 자리 | API, 마이크로서비스, 서드파티 검증 | 단일 백엔드 앱, 민감한 세션 |
여러 모범 사례 가이드가 이제 첫머리에 두는 유행에 맞지 않는 진실이 있습니다. 백엔드가 하나인 서버 렌더링 앱에는 프레임워크 세션이 더 단순하고 동시에 더 통제하기 쉽습니다. JWT는 토큰을 여러 서비스에 걸쳐, 또는 요청마다 발급자에게 되물어서는 안 되는 당사자가 검증해야 할 때 제값을 합니다.
폐기 문제, 솔직하게
무상태는 곧 폐기 불가능입니다. 같은 속성을 두 번 말한 것입니다. 서명된 토큰은 그 뒤에 무슨 일이 있었든(로그아웃, 비밀번호 변경, 계정 정지) exp까지 유효합니다. 모든 해결책은 상태를 다시 도입하므로, 어떤 종류의 상태를 택할지 고르는 문제입니다. jti + iss를 키로 하는 거부 목록(OWASP가 권장하는 구성)을 요청마다 검사하는 방법은 작지만 어쨌든 상태입니다. 키 버전 관리는 모든 사람을 한꺼번에 폐기합니다. 그리고 표준 아키텍처는 짧은 수명의 액세스 토큰(5–15분)과 폐기 가능한 리프레시 토큰에 회전을 더한 것입니다. 리프레시할 때마다 새 리프레시 토큰을 발급하고 이전 것을 폐기하므로, 도난당한 토큰은 첫 재사용에서 소멸하고, 폐기된 토큰의 재사용은 도난을 알리며 토큰 계열 전체를 소멸시킵니다. 그러면 폐기 지연은 액세스 토큰의 수명과 같아지며, 이것이 그 수명이 짧은 진짜 이유입니다.
알고리즘 선택, 그리고 그 선택을 노리는 공격
| HS256(HMAC) | RS256(RSA) | Ed25519 / ES256 | |
|---|---|---|---|
| 키 | 공유 비밀 하나 | 개인 키로 서명, 공개 키로 검증 | 개인 키로 서명, 공개 키로 검증 |
| 검증자에게 필요한 것 | 비밀(위조도 가능!) | 공개 키만 | 공개 키만 |
| 적합한 경우 | 발급자 = 검증자, 단일 주체 | 다중 서비스, 서드파티 | 동일, 더 작고 빠른 서명 |
| 날카로운 모서리 | 짧은 비밀은 캡처한 토큰 하나로 오프라인 무차별 대입 가능 – 256비트 이상의 무작위 값 사용 | 더 큰 토큰, 더 느림 | 라이브러리 지원이 비교적 최근 |
RFC 8725가 존재하는 이유인 공격 부분을 세 문장으로 요약합니다. alg: "none"은 합법적인 헤더 값이며, 이를 존중하는 라이브러리는 서명 없는 토큰을 받아들입니다. 혼동 공격은 토큰의 헤더가 알고리즘을 고르게 두는 검증자에게 서버의 공개 RSA 키를 HMAC 비밀로 써서 서명한 “HS256” 토큰을 건네는 것입니다. 공개 값이 비밀 값으로 쓰이는 셈입니다. 둘은 같은 방법으로 막힙니다. 검증자는 허용하는 알고리즘과 키를 설정에 고정하고, 헤더가 선택하도록 절대 맡기지 않습니다.
브라우저에서 JWT를 저장할 위치
| 위치 | XSS로 탈취되는가? | CSRF로 전송되는가? | 새로고침 후 유지되는가? | 판정 |
|---|---|---|---|---|
| localStorage | 예 | 아니요 | 예 | 피해야 함 – 스크립트가 읽을 수 있음 |
| 일반 쿠키 | 예(스크립트가 읽을 수 있음) | 예 | 예 | 양쪽의 단점만 모음 |
| HttpOnly + Secure + SameSite 쿠키 | 아니요 | SameSite로 완화 | 예 | 좋음 – 리프레시 토큰용 |
| 메모리 | 주입된 동안만 | 아니요 | 아니요 | 좋음 – 액세스 토큰용 |
| backend-for-frontend가 토큰 보관 | 브라우저가 가진 적 없음 | 쿠키 규칙 적용 | 예 | SPA에 가장 강력 |
속설이 아니라 위협 모델로 보면, localStorage는 CSRF 면역을 XSS 탈취와 맞바꾸고 쿠키는 그 반대를 맞바꿉니다. 그래서 합의는 둘을 나눕니다. 액세스 토큰은 메모리에, 리프레시 토큰은 강화된 쿠키에 둡니다.
일반적인 사용 사례
- API 인증 – REST와 GraphQL API의
Authorization헤더 뒤에 있는 베어러 토큰. - 마이크로서비스 ID – 게이트웨이가 발급한 토큰 하나를 모든 서비스가 독립적으로 검증하며, 공유 세션 저장소가 없습니다.
- OpenID Connect ID 토큰 – 모든 소셜 로그인에 들어 있는 서명된 ID 어설션으로, 항상 JWT입니다.
- 시스템 간 전달 – 서명된 링크, 웹훅 페이로드, 다운로드 허가. 되묻을 수 없는 당사자가 검증하는 클레임입니다.
- 무상태 인가 힌트 – 클레임에 실린 역할과 테넌트 ID. 권위 있는 검사는 여전히 서버 측에 있습니다.
JWT와 세션 중 무엇을 사용해야 할까? 결정 매트릭스
| 상황 | 선택 |
|---|---|
| 단일 백엔드, 서버 렌더링 앱 | 세션 – 더 단순하고 즉시 폐기 가능 |
| 여러 서비스가 소비하는 공개 API | JWT, 비대칭 키 |
| 게이트웨이 뒤의 마이크로서비스 | JWT – 로컬에서 검증, 공유 저장소 없음 |
| 즉각적인 잠금이 필수 요건 | 세션, 또는 JWT + 거부 목록과 짧은 exp |
| 서드파티가 자사의 어설션을 검증해야 함 | JWT – 이 형식의 홈그라운드 |
| BaaS를 쓰는 모바일 앱 | 플랫폼의 세션 메커니즘 – 이미 선택되어 있음 |
한계와 트레이드오프
- 폐기 불가능은 구조적입니다. 거부 목록, 짧은 수명, 회전 등 모든 완화책은 제거했던 상태를 일부 되돌리는 것입니다. 무상태를 선택하기 전에 이 비용을 계산에 넣어야 합니다.
- 클레임은 낡아 갑니다. 역할은 발급 시점의 스냅샷이며, 강등된 관리자는 만료까지 관리자로 남습니다. 짧은 수명이 낡음의 범위를 제한합니다.
- 페이로드는 공개입니다. 사용자와 토큰을 훔친 누구든 읽을 수 있다고 간주해야 합니다. 식별자는 되지만 비밀과 개인 정보는 안 됩니다.
- 라이브러리 기본값에 데인 사람이 많습니다. 알고리즘 고정, 클레임 확인,
typ검사는 라이브러리가 보장하는 것이 아니라 사용자가 설정해야 하는 책임입니다. - 크기는 누적됩니다. 모든 클레임이 모든 요청에 실려 다니므로, 넉넉한 페이로드는 모바일 대역폭에 부담을 주고 쿠키 한도를 넘길 수 있습니다.
Back4app에서의 토큰
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 플랫폼 자체의 선택이 결정 매트릭스를 보여 줍니다. 클라이언트 세션은 위의 코드 탭처럼 폐기 가능한 서버 측 세션 토큰을 사용하므로, 로그아웃, 비밀번호 변경, 대시보드에서의 세션 종료가 기다려야 할 만료 기간 없이 즉시 적용됩니다. JWT는 제자리에 등장합니다. ID 공급자의 OIDC ID 토큰은 소셜 로그인 중 인증 어댑터가 서버 측에서 검증하고, Cloud Code 함수는 표준 라이브러리로 서드파티 전달용 JWT를 발급하거나 검증할 수 있습니다. 서명 비밀은 서버 측 설정에 두며 클라이언트에는 절대 전달하지 않습니다. 통제가 중요한 곳에서는 상태 유지, 검증이 이동해야 하는 경계에서는 무상태. 이 글이 주장하는 아키텍처가 미리 조립되어 있습니다.
자주 묻는 질문
JWT를 쉽게 설명하면 무엇입니까?
서버가 로그인 시 발급하는, 서명되고 Base64로 인코딩된 JSON "신분증"입니다. 클라이언트는 이를 모든 요청에 제시하고, 서버는 세션을 조회하는 대신 서명을 검증합니다. 토큰 자체가 사용자가 누구이고 언제까지 유효한지를 담고 있습니다. 이를 정의한 RFC에 따르면 영어 단어 "jot"처럼 발음합니다.
JWT의 세 부분은 무엇입니까?
헤더(토큰 유형과 서명 알고리즘), 페이로드(클레임, 즉 실제 JSON 데이터), 서명입니다. 각각 Base64Url로 인코딩되어 점으로 이어진 header.payload.signature 형태입니다. 서명은 앞의 두 부분에 대해 계산되므로, 그 부분을 조금이라도 변조하면 검증이 실패합니다.
JWT는 암호화되어 있습니까?
아닙니다. 인코딩된 것이지 암호화된 것이 아닙니다. Base64Url은 누구나 되돌릴 수 있는 전송 형식이며, 아무 JWT나 디코더에 붙여 넣으면 페이로드를 읽을 수 있습니다. 서명은 변조를 막을 뿐 읽기를 막지 않습니다. JWT 페이로드에 비밀이나 민감한 개인 정보를 절대 넣어서는 안 되며, 진정한 기밀성이 필요하다면 암호화 변종인 JWE가 있습니다.
JWT와 세션 토큰의 차이는 무엇입니까?
상태가 어디에 있는가의 차이입니다. 세션 토큰은 서버 측 상태를 가리키는 불투명한 ID로, 요청마다 한 번 조회하며 즉시 폐기할 수 있습니다. JWT는 상태를 자기 안에 담고 있어 조회가 없고 키를 가진 어떤 서비스든 검증할 수 있지만, 무슨 일이 있어도 만료 전까지는 유효합니다. 무상태 확장 대 즉각적인 통제입니다.
브라우저에서 JWT는 어디에 저장해야 합니까?
localStorage는 아닙니다. 주입된 어떤 스크립트든 읽을 수 있어 XSS가 곧 토큰 탈취가 됩니다. 현재의 합의는 액세스 토큰은 메모리에, 리프레시 토큰은 HttpOnly, Secure, SameSite 쿠키에 두는 것이며, 토큰을 브라우저 밖에 완전히 두는 backend-for-frontend 패턴도 고려할 만합니다.
JWT를 폐기할 수 있습니까?
설계상으로는 불가능합니다. 서명된 토큰은 exp 클레임까지 유효하며, 이것이 무상태의 대가입니다. 우회책은 모두 상태를 다시 도입합니다. jti 클레임을 키로 하는 거부 목록, 키 버전 관리, 또는 표준적인 답인 매우 짧은 액세스 토큰 수명과 폐기 가능하고 회전되는 리프레시 토큰의 조합입니다.
HS256과 RS256의 차이는 무엇입니까?
대칭성입니다. HS256은 하나의 공유 비밀로 서명하고 검증하므로, 발급자와 검증자가 같은 주체이고 길고 무작위적인 비밀을 쓸 때만 적합합니다. RS256(그리고 더 새로운 Ed25519)은 개인 키로 서명하고 누구나 공개 키로 검증하므로, 둘 이상의 서비스가 토큰을 검사한다면 기본 선택입니다.
JWT와 OAuth의 차이는 무엇입니까?
범주가 다릅니다. JWT는 토큰 형식이고 OAuth 2.0은 인가 프레임워크입니다. 둘은 조합됩니다. OAuth 흐름은 흔히 JWT 형식의 액세스 토큰을 발급하며, OpenID Connect ID 토큰은 항상 JWT입니다. 하나는 토큰을 어떻게 만드는지, 다른 하나는 토큰을 어떻게 나눠 주는지를 말합니다.