JWT (JSON Web Token) 는 두 시스템 사이에서 정보를 주고받는 데 쓰는 짧은 문자열이다. 로그인 직후 서버가 발급한 한 줄짜리 토큰을 떠올리면 된다. 이 가이드는 JWT 가 어떻게 생겼고, 검증은 어떻게 해야 하며, 실전에서 흔히 빠지는 보안 함정이 무엇인지 다룬다. RFC 7519 는 분량이 적지 않으니 실제로 쓰는 데 필요한 핵심만 골라 정리했다.
JWT 가 풀어주는 문제
전통적인 세션 방식은 서버가 세션 ID 를 저장하고 매 요청마다 데이터베이스 혹은 캐시에서 조회한다. 사용자 수가 늘면 세션 스토어가 병목이 되고, 여러 서버에 분산하면 sticky session 또는 공유 캐시가 필요해진다.
JWT 는 사용자 정보를 토큰 자체에 담아 서명한다. 서버는 토큰만 검증하면 되므로 별도 조회가 필요 없다. 마이크로서비스 사이에서도 같은 토큰을 주고받으면 인증 정보가 그대로 흐른다. 단, 토큰 자체에 정보가 박혀 있으므로 일단 발급된 토큰은 만료 전까지 회수가 어렵다 — 이 트레이드오프가 JWT 설계의 핵심이다.
토큰 구조 — 세 조각
JWT 는 점(.) 으로 구분된 세 조각이다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.SflKxw...각 조각은 다음과 같다.
- Header — Base64URL 인코딩된 JSON. 알고리즘(
alg) 과 타입(typ) 을 담는다. 예:{"alg":"HS256","typ":"JWT"} - Payload — Base64URL 인코딩된 JSON. 사용자 ID·만료 시간·권한 같은 claim 을 담는다.
- Signature — Header 와 Payload 를 합쳐 시크릿 키로 서명한 결과. 변조 감지에 쓴다.
Base64URL 은 일반 Base64 와 거의 같은데 URL 안전 문자만 쓴다 (+ → -, / → _, 패딩 = 생략). 인코딩만 됐을 뿐 암호화는 아니라는 점이 중요하다 — Base64 인코딩 / 디코딩 로 payload 만 디코드하면 누구나 내용을 볼 수 있다. payload 에 민감 정보를 넣으면 안 된다.
흔히 쓰는 표준 claim
RFC 7519 가 정한 표준 claim 은 짧은 키를 쓴다 — 토큰 크기를 줄이기 위함.
| 키 | 이름 | 의미 |
|---|---|---|
iss | Issuer | 발급자 (예: 인증 서버 URL) |
sub | Subject | 토큰이 가리키는 주체 (보통 user id) |
aud | Audience | 토큰을 받는 대상 (API 이름 등) |
exp | Expiration | 만료 시각 (Unix epoch 초) |
nbf | Not Before | 이 시각 이전엔 유효하지 않음 |
iat | Issued At | 발급 시각 |
jti | JWT ID | 토큰 고유 식별자 (revocation list 용) |
커스텀 claim 은 자유롭게 추가할 수 있다 — role, email 등. 단, payload 크기가 커지면 매 요청마다 헤더로 실어 나르는 비용이 커진다. 권장은 200~500 바이트 이하.
알고리즘 — HS256 vs RS256
alg 헤더가 서명 방식을 결정한다. 실무에서 거의 다 다음 둘 중 하나다.
- HS256 (HMAC-SHA256) — 공유 비밀 키로 서명·검증. 서버 쪽 단일 시스템에서 빠르고 단순. 시크릿이 노출되면 누구나 토큰을 위조할 수 있다. HMAC 생성기 의 동작 원리와 동일.
- RS256 (RSA-SHA256) — 비대칭. 발급자는 private key 로 서명, 수신자는 public key 로 검증. 공개 키만 배포하면 되므로 다중 서비스 사이에 안전하게 검증 권한을 나눌 수 있다. OAuth/OIDC 프로바이더 (Auth0, Cognito 등) 의 기본값.
ES256 (ECDSA) 도 RS256 과 같은 비대칭이지만 키·서명이 짧다. 모바일 클라이언트나 토큰 크기가 중요한 환경에서 선호된다.
검증 — 반드시 해야 할 일
JWT 디코더 같은 도구로 토큰을 풀면 payload 가 즉시 보인다. 하지만 디코딩과 검증은 다르다. 서버에서 토큰을 받아들이려면 다음을 모두 확인해야 한다.
- 서명 검증 — header 가 선언한 알고리즘으로 시크릿/공개 키를 적용해 signature 가 일치하는지.
- 알고리즘 강제 — 서버가 받는 알고리즘 목록을 화이트 리스트로 박아둔다. 절대로 header 의
alg를 그대로 믿지 마라 — 이게 가장 유명한 JWT 공격(alg: none공격, HS256/RS256 confusion) 의 진입점이다. exp확인 — 현재 시각이exp보다 작아야 한다. 시계 차이를 감안해 ±30 초 정도 leeway 를 허용하는 라이브러리가 많다.iss/aud일치 — 토큰이 의도한 발급자가 만들었고, 의도한 대상(우리 서비스) 을 위한 것인지.
대부분의 라이브러리 (Node 의 jsonwebtoken, Python 의 PyJWT, Go 의 golang-jwt) 는 위 항목을 한 번에 처리하지만 verify(token, secret, [{algorithms: ["HS256"]}]) 식으로 알고리즘을 명시하지 않으면 기본값이 위험할 수 있다.
자주 빠지는 함정
1. payload 에 민감 정보 넣기
앞서 말한 대로 payload 는 단순 인코딩이다. 비밀번호, 주민번호, API 키 등 절대 금지. 적어도 사용자 ID 와 권한 정도만 넣고, 민감 정보는 서버에서 ID 로 다시 조회한다.
2. 시크릿 키가 너무 짧음
HS256 의 시크릿이 사전 단어 한 줄이면 brute force 로 뚫린다. 최소 256 비트 (32 바이트) 의 랜덤 바이트를 권장 — openssl rand -base64 32 또는 password generator 로 생성한 긴 임의 문자열을 쓴다. 시크릿이 코드에 박혀 있으면 git 검색 한 번으로 노출된다.
3. 토큰 회수 불가
JWT 는 stateless 가 장점이지만, "로그아웃 즉시 모든 세션 무효화" 같은 요구는 어렵다. 해결책은 두 가지.
- 짧은 만료 시간 + refresh token — access token 은 5~15 분, refresh token 은 서버 DB 에 보관해 회수 가능.
- revocation list —
jti또는 사용자 ID 를 블랙리스트에 박고 검증 시 조회. stateless 이점이 사라지지만 보안 요구가 높다면 합리적.
4. alg: none 공격
초기 JWT 라이브러리 일부가 alg 가 none 이면 signature 검증을 건너뛰는 동작을 했다. 공격자가 header 를 {"alg":"none"} 으로 바꾸고 payload 를 위조한다. 2015~2019 사이 다수의 라이브러리가 패치됐지만 옛 버전이 남아 있는 환경은 여전히 위험.
5. HS256 ↔ RS256 confusion
서버가 RS256 으로만 받는다고 가정했지만 공격자가 header 를 HS256 으로 바꾸고 공개 키를 HS256 의 시크릿으로 사용해 서명한다. 공개 키는 대개 노출돼 있으므로 누구나 토큰을 위조할 수 있다. 알고리즘 화이트리스트를 명시하는 것이 유일한 방어.
6. 시계 차이
분산 시스템에서 서버 시계가 몇 초씩 어긋나면 방금 발급한 토큰이 exp 또는 nbf 검증에서 거부될 수 있다. NTP 동기화 + ±30 초 leeway 가 표준.
저장 위치 — 어디에 둘까
브라우저에서는 localStorage, sessionStorage, 또는 쿠키 중 하나를 쓴다.
- localStorage — JS 로 접근 가능, XSS 한 번이면 모든 토큰이 털린다. 가장 단순하지만 가장 위험.
- HttpOnly + Secure 쿠키 — JS 접근 불가. XSS 로는 못 뽑지만 CSRF 방어가 추가로 필요. SameSite=Strict/Lax 와 CSRF 토큰 병용 권장.
- 메모리 (변수) — 새로고침 시 사라지지만 가장 안전. SPA 에서 짧은 access token 을 메모리에 두고, refresh 는 HttpOnly 쿠키로 하는 패턴이 일반적.
도구로 직접 보기
JWT 가 처음이라면 직접 만들어 보고 풀어 보는 게 가장 빠르다.
- JWT 생성기 (HMAC) — payload·시크릿·알고리즘을 입력해 토큰을 직접 발급. HS256/HS384/HS512 지원.
- JWT 디코더 — 받은 토큰을 풀어 header·payload 를 본다. 모든 처리가 브라우저 안에서 끝나므로 시크릿 토큰을 외부 서비스에 보낼 필요가 없다.
- HMAC 생성기 — HS256 의 서명이 어떻게 만들어지는지 직접 HMAC-SHA256 으로 계산해 보면 토큰 마지막 조각이 그대로 나온다.
요약
- JWT = Base64URL(header).Base64URL(payload).signature
- payload 는 암호화가 아니라 인코딩일 뿐 — 민감 정보 금지.
- 검증 시 알고리즘 화이트리스트 +
exp/iss/aud모두 확인. - 시크릿은 256 비트 이상의 임의 문자열. 코드에 박지 말고 환경 변수/시크릿 매니저로.
- 짧은 access token + refresh token 패턴이 stateless 와 회수 가능성 사이의 합리적 절충.