Developer Tools

JWT 토큰 완벽 해설: 구조, 보안, 그리고 흔한 실수들

JSON Web Token의 작동 방식, 내부 구조, 그리고 프로덕션 환경에서 개발자들이 빠지기 쉬운 보안 함정을 알아봅니다.

8분 읽기

회로 기판 위의 보안 자물쇠

JSON Web Token(JWT)은 어디에나 있습니다 — SPA, 모바일 앱, 전 세계 마이크로서비스 아키텍처의 인증을 담당하고 있죠. 그러나 동시에 웹 개발에서 가장 많이 오해받는 보안 기본 요소 중 하나이기도 합니다. 잘못 사용하면 인증 우회, 권한 상승, 또는 계정 완전 탈취로 이어질 수 있습니다.

JWT란 무엇인가?

JWT는 클레임(claims) — 사용자나 세션에 대한 정보 — 을 인코딩한 간결하고 URL-safe한 문자열입니다. 다음과 같이 생겼습니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

점(.)으로 구분된 세 개의 Base64URL 인코딩 세그먼트로 구성됩니다:

  1. Header — 알고리즘과 토큰 타입
  2. Payload — 클레임 (사용자 데이터)
  3. Signature — 무결성을 증명하는 암호화 서명

우리의 JWT Decoder에 JWT를 붙여넣으면 외부 서버를 거치지 않고 세 부분을 즉시 확인할 수 있습니다.

JWT 구조 분석

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

alg는 서명 알고리즘을 지정합니다. 주요 값:

  • HS256 — HMAC with SHA-256 (대칭키, 단일 시크릿)
  • RS256 — RSA with SHA-256 (비대칭키, 공개/개인 키)
  • ES256 — ECDSA with SHA-256 (비대칭키, 더 짧은 키)

Payload

{
  "sub": "user_abc123",
  "email": "alice@example.com",
  "role": "admin",
  "iat": 1711670400,
  "exp": 1711756800
}

표준 등록 클레임:

클레임 의미
sub Subject (토큰의 대상)
iss Issuer (토큰 발급자)
aud Audience (토큰 수신 대상)
exp Expiration time (Unix 타임스탬프)
iat Issued at (Unix 타임스탬프)
nbf Not before (이 시간 이전에는 수락 불가)

Signature

HS256의 경우:

HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)

서명은 토큰이 변조되지 않았음을 증명합니다. Payload는 암호화되지 않으며 — 단순히 인코딩된 것입니다. 누구든 읽을 수 있습니다.

비밀번호나 신용카드 번호 같은 민감한 데이터를 JWT payload에 저장하지 마세요.

HS256 vs. RS256: 무엇을 선택해야 할까?

HS256은 하나의 공유 시크릿을 사용합니다. 토큰을 검증해야 하는 모든 서비스가 동일한 시크릿을 보유해야 합니다. 단일 서버 환경에서는 간단하지만, 멀티 서비스 아키텍처에서는 위험합니다 — 서비스 하나가 침해되면 공격자가 토큰을 위조할 수 있습니다.

RS256은 비대칭 키를 사용합니다. 인증 서버는 개인 키로 서명하고, 다른 모든 서비스는 공개 키로 검증합니다. 소비자 서비스가 침해되더라도 공격자가 토큰을 위조할 수 없습니다. 여러 서비스가 있는 시스템에서는 RS256을 권장합니다.

alg: none 취약점

가장 악명 높은 JWT 공격 중 하나입니다. 일부 구버전 라이브러리는 "alg": "none"이고 서명이 없는 토큰을 유효한 것으로 처리했습니다. 공격자는 다음과 같이 조작할 수 있었습니다:

{ "alg": "none" }

어떤 payload든 붙여서 인증을 완전히 우회할 수 있었습니다.

해결책: JWT 라이브러리에서 허용할 알고리즘을 항상 명시적으로 지정하세요. none은 절대 허용하지 마세요.

// ❌ 위험
jwt.verify(token, secret);

// ✅ 안전 — HS256만 허용
jwt.verify(token, secret, { algorithms: ["HS256"] });

알고리즘 혼동 공격

또 다른 심각한 취약점: 라이브러리가 헤더에서 알고리즘을 자동으로 감지한다면, 공격자는 RS256HS256으로 변경하고 공개 키를 HMAC 시크릿으로 사용하여 토큰에 서명할 수 있습니다 (공개 키는 말 그대로 공개되어 있으므로).

해결책: 기대하는 알고리즘을 항상 하드코딩하세요. alg 헤더를 절대 신뢰하지 마세요.

토큰 저장: JWT를 어디에 보관할까?

저장 방식 XSS 위험 CSRF 위험 비고
localStorage 높음 없음 페이지의 모든 JS에서 접근 가능
sessionStorage 높음 없음 탭 닫으면 삭제
HTTP-only 쿠키 없음 중간 웹 앱에 최적; SameSite=Strict 사용
메모리 (변수) 낮음 없음 새로고침 시 소실; SPA에 적합

웹 애플리케이션의 경우, SameSite=Strict를 설정한 HTTP-only 쿠키가 가장 안전한 옵션입니다. 네이티브 앱의 경우 보안 저장소 API(Keychain, Keystore)가 적합합니다.

만료와 리프레시 토큰

단기 액세스 토큰(5~15분)과 장기 리프레시 토큰을 함께 사용하는 것이 표준 패턴입니다:

  1. 사용자 로그인 → 서버가 액세스 토큰(15분) + 리프레시 토큰(7일, DB 저장) 발급
  2. 클라이언트는 API 호출에 액세스 토큰 사용
  3. 액세스 토큰 만료 시 클라이언트가 리프레시 토큰 전송 → 새 액세스 토큰 수령
  4. 로그아웃 시 데이터베이스에서 리프레시 토큰 무효화

이 방식은 액세스 토큰이 탈취되더라도 피해 범위를 제한합니다.

JWT 폐기하기

JWT는 무상태(stateless)입니다 — 한번 발급되면 만료 전까지 유효합니다. 이는 트레이드오프입니다. 조기 폐기를 위한 옵션:

  • 차단 목록(Blocklist) — 무효화된 JTI(JWT ID) 값을 Redis에 저장하고 매 요청마다 확인합니다.
  • 짧은 만료 시간 — 5분 토큰은 피해 범위를 제한합니다.
  • 리프레시 토큰 로테이션 — 이미 교체된 토큰의 재사용을 탈취 징후로 감지합니다.

JWT Decoder로 디버깅하기

인증 문제를 디버깅할 때 우리의 JWT Decoder를 활용하세요:

  • 코드 없이 전체 payload 확인
  • 만료 타임스탬프를 사람이 읽기 쉬운 형태로 확인
  • 사용 중인 알고리즘 검증
  • 예상치 못한 클레임이나 누락된 클레임 발견

모든 디코딩은 브라우저 내에서 로컬로 처리됩니다 — 토큰이 외부로 전송되지 않습니다.

JWT 보안 체크리스트

  • 멀티 서비스 아키텍처에서는 RS256 또는 ES256 사용
  • 라이브러리에서 허용 알고리즘을 명시적으로 지정
  • 모든 요청에서 exp, iss, aud 클레임 검증
  • 웹 앱에서는 HTTP-only 쿠키에 토큰 저장
  • 짧은 액세스 토큰 유효기간 사용 (15분 이하)
  • 재사용 감지 기능이 포함된 리프레시 토큰 로테이션 구현
  • payload에 민감한 데이터 절대 저장 금지
  • 모든 곳에서 HTTPS 사용 — 평문 토큰은 인증이 없는 것과 같음

JWT는 강력하고 편리하지만, 구현 방식에 따라 보안 수준이 결정됩니다. 구조를 이해하고 공격 방식을 숙지한다면, 여러분의 인증 레이어는 견고해질 것입니다.