JWT란?
JOSE 패밀리
먼저 JOSE에 대해서 알아보자. JOSE(JavaScript Object Signing and Encryption)란 JSON 데이터를 서명하거나 암호화하는 전체 표준군의 총칭이고 구성요소는 다음과 같다.
JWS (RFC 7515): JSON에 디지털 서명(무결성/진위성)을 붙이는 규격
JWE (RFC 7516): JSON을 암호화(기밀성+무결성)하는 규격
JWK (RFC 7517): 키(대칭/공개/개인)를 JSON으로 표현하는 포맷. 묶음은 JWKS
JWA (RFC 7518): JOSE에서 쓰는 알고리즘 레지스트리(HS256, RS256, A256GCM 등)
JWT (RFC 7519): JSON 클레임 세트를 안전하게 전달하는 표준 포맷
엑세스 토큰은 누가 발급했는지와 내용이 변조되지 않았는지만 빠르게 검증하면 되기 때문에 거의 대부분 JWS기반 JWT를 사용한다. 자세한 내용은 아래에서 확인해보자.
구조
대중적인 JWS를 기반으로 JWT의 구조를 살펴보자.
header.payload.signature 세 부분으로 이루어져있다. 각 부분에 담기는 내용은 다음과 같다.
header: { "alg": "RS256", "kid": "2025-09-01" }
payload: 내용(Claims)
signature: sign( base64url(header) + "." + base64url(payload) )
여기서 header에 들어가는 alg는 사용된 JWS 서명 알고리즘을 의미한다. 다음과 같은 종류들이 존재한다.
- 대칭: HS256/384/512 (HMAC-SHA2)
- RSA: RS256/384/512 (PKCS#1 v1.5), PS256/384/512 (RSA-PSS)
- EC: ES256/384/512 (P-256/384/521 + ECDSA)
- OKP: EdDSA (대부분 Ed25519)
header와 payload를 BASE64URL로 인코딩한 후 '.'으로 이은 두 문자열에 대해 명시된 서명 알고리즘을 통해 signature를 생성하여 붙인 후 JWT를 발급한다.
Claims
위에서 Payload에 클레임(Claims)이 들어간다고 했는데, 클레임에 대해서 알아보자. Claim의 명사 뜻은 "주장, 요구"로 우리가 일상에서 자주 사용하는 그 Claim이다. 여기서는 JWT 발급자가 어떤 주체에 대해서 무엇을 주장하는지를 담은 JSON의 필드 모음을 말한다.
세 가지 네임스페이스가 있으며 각 네임스페이스는 다음과 같이 구분한다.
1. Registered: RFC 7519에 명시되어있는 표준 클레임
2. Public: IANA에 등록된 공개 클레임 (거의 사용하지 않음)
3. Private: 서비스가 임의로 정한 커스텀 클레임
Registered 클레임의 규약은 다음과 같다.
| iss | String | 발급자 식별자. 여러 값 허용 X. |
| sub | String | 주체(사용자) ID. 불변·영속 ID 권장(DB PK 등). 이메일/닉네임 금지(변경됨). |
| aud | String or String[] | 수신자(대상 리소스 / 발급 서버가 분리되어있는 경우). 내 서비스 식별자가 포함되어야 “나에게 온 토큰”. 문자열 하나거나 배열일 수 있음. |
| exp | NumericDate | 만료 시각(초 단위 Unix time). 항상 짧게(5~15분). 밀리초가 아님! |
| nbf | NumericDate | Not Before. 이 시각 전엔 무효. 시계 스큐로 인한 오탐 방지용으로 leeway(오차)와 함께 사용. |
| iat | NumericDate | 발급 시각. 디버깅·이상 징후 탐지용. 단독 신뢰 금지(맞춰놓지 않은 시계도 많음). |
| jti | String | 토큰 고유 ID. 블랙리스트/재발급 추적/리플레이 탐지에 사용. 보통 UUIDv4. |
하지만 이 값들은 필수가 아니며 이런 값들이 있다~ 정도만 알고 넘어가면 될 것 같다.
위 값들에 더해 우리가 JWT에 담고 싶은 값들을 커스텀으로 세팅해서 클레임으로 담을 수 있다.
서명 알고리즘
위에서 여러 서명 알고리즘을 확인해보았다. 자주 사용하는 알고리즘과 그 이유를 알아보자.
| 대칭(HMAC) | HS256/384/512 | 발급자와 검증자가 같은 비밀키 공유. 빠르고 간단하지만, 키가 유출되면 누구나 토큰을 위조 가능. | 키 비밀(≥256bit) |
| RSA | RS256/384/512 (PKCS#1 v1.5), PS256/384/512 (RSA-PSS) | 개인키로 서명/공개키로 검증. 호환성 최고. 서명 문자열이 큼. PSS가 최신 권장. | 2048/3072/4096 bit |
| 타원곡선/현대 서명 | ES256/384/512 (ECDSA), EdDSA(Ed25519/Ed448) | 짧고 빠른 서명. 토큰이 짧아짐. Ed25519는 단순·고성능. 일부 환경은 FIPS/정책 확인 필요. | P-256/384, Ed25519 |
대칭키는 한 개의 비밀키를 발급자와 모든 검증자가 공유하기 때문에 발급자처럼 토큰을 만들 수 있다는 단점이 있어, 보통은 비대칭키를 사용한다. 하지만 서명/검증 비용과 서명 길이가 상대적으로 길어진다는 단점도 있다.
JWS vs JWE
액세스 토큰은 서버 전달용 "권한 증표"에 가깝기 때문에 기밀성보다는 무결성/진위성이 핵심이다.
- JWS(서명): 클레임이 평문으로 보인다(서명만). 대부분의 “액세스 토큰”은 JWS.
- JWE(암호화): 클레임 비공개 필요 시. RSA-OAEP-256 + A256GCM 조합이 흔함.
- 압축(zip): JWE에서만 표준. JWS에 임의 zip 쓰는 구현은 상호운용성 이슈.
JWS - 서명된 JWT
- 형태: header.payload.signature (3파트)
- 헤더 예시: {"alg":"RS256","kid":"...","typ":"JWT"}
- 목적: 무결성/진위성(누가 발급했는지 증명). 대부분의 액세스 토큰이 이 형태이다.
JWE-암호화된 JWT
- 형태: header.encryptedKey.iv.ciphertext.tag (5파트)
- 헤더 예시: {"alg":"RSA-OAEP-256","enc":"A256GCM","kid":"..."}
- 목적: 기밀성(내용 숨김) + AEAD로 무결성. 다만 발급자 비가역 서명이 아니어서, 암호 키를 가진 누구나 만들 수 있다는 점에서 JWS와 신뢰 의미가 다르다.
- Nested JWT: JWS로 먼저 서명하고 그 JWS를 JWE로 암호화하는 방법이다. 이때 JWE 헤더에 cty: "JWT"(콘텐츠 타입)로 안에 JWT가 있음을 표시한다.
대부분은 JWS - 검증만으로 신뢰를 보장하기 때문에 가장 광범위하게 지원한다. 하지만 민감 정보를 포함하는 경우에는 JWE를 사용한다.
액세스 토큰 vs 리프레시 토큰
JWT로 만든 액세스 토큰은 탈취당하면 해당 토큰이 만료될 때 까지 탈취자가 계속해서 사용할 수 있다. 해당 문제를 해결하기 위해 리프레시 토큰이 도입되었다. 액세스 토큰은 짧게(분 단위)가져가고 리프레시 토큰은 길게(일~주 단위) 가져간다.
또한 브라우저의 저장소 전략도 고려해보아야 한다.
HttpOnly + Secure + SameSite=Lax 쿠키: XSS로부터 비교적 안전하지만, CSRF 방어가 필요하다.
* 크로스 사이트 OIDC 리다이렉트 등 필요하면 SameSite=None; Secure.
* __Host- 프리픽스 쿠키: 하위도메인 주입 방지.
localStorage: CSRF엔 유리하지만 XSS 취약. 절대 토큰을 노출 로그/에러에 남기지 말아야 한다.
메모리 저장: 페이지 새로고침 시 소실되어 백업/재발급 로직이 필요하다.
그래서 왜 사용하나요?
정리해보자면 JWT는 "서명된 주장(클레임) 묶음”을 표준 포맷으로 전달하기 위해 쓴다. 즉, 누가 발급했고(iss) 내용이 변조되지 않았는지를 다른 서버가 오프라인으로 빠르게 검증하기 위한 수단이다.
무상태(Stateless)로 신뢰 가능한 사용자/클라이언트 정보를 전달할 수 있다. 따라서 중앙 세션 공유없이 수평적인 서버 스케일 아웃을 편하게 할 수 있다는 장점이 있다. 또한 scope, roles, aud와 같은 클레임으로 권한과 대상(API)을 명시하여 명세화된 권한을 전달할 수 있다.
대표적으로 다음과 같은 상황들에서 유용하게 사용할 수 있다고 할 수 있다.
- 퍼블릭 API: 리소스 서버가 세션 저장소 없이 서명 검증만으로 요청을 처리할 수 있기 때문에 글로벌 트래픽에도 수평 확장이 쉽다. 또한 무엇을, 어느 API에 허용했는지 토큰 자체에 계약으로 명시해두었기 때문에 검증하기 편하다.
- 마이크로서비스 간 호출: 서비스 간에 세션을 돌리지 않아도 JWT 하나로 신원 컨텍스트 전파가 가능하다.
VS Session
그럼 사용자/클라이언트 정보를 확인하기 위해 쓰는 또 다른 수단인 Session 방식과 비교해보자.
- 세션: 로그인 성공 시 서버가 세션 저장소(메모리/Redis/DB 등)에 사용자 상태를 기록하고, 클라이언트에 세션 ID(일반적으로 쿠키)만 전달. 요청마다 서버 저장소를 조회해 사용자 컨텍스트를 복원한다. 따라서 서버가 상태를 들고 있어 즉시 회수/폐기가 쉽고, SSR/BFF에 적합하다. 단, 세션 공유/스케일에 추가 인프라가 필요하다는 단점이 있다.
- JWT: 로그인 성공 시 서버가 사용자 클레임을 서명해 액세스 토큰(JWT) 자체에 담아 돌려주고 이후 요청은 토큰 자체를 검증해 컨텍스트를 복원(서버 저장소 불필요)한다. 상태 없이 검증만으로 처리해 확장성이 좋다(하지만 회수/리프레시 등은 상태가 필요할 수 있음). 대신 회수/권한 변경 반영은 전략(짧은 TTL, jti 블랙리스트, RT 회전)이 필요하는 단점이 있다.
다음과 같은 확장성과 운영 포인트들을 고려하여 시스템을 설계해볼 수 있다.
| 세션 | JWT | |
| 스케일 아웃 | 세션 공유 필요 → Redis/DB or 스티키 세션 | 무상태 검증으로 수평 확장 쉬움 |
| 네트워크 비용 | 매 요청 세션 조회 I/O | 매 요청 서명 검증 계산만 필요 |
| 장애/캐시 | Redis 장애 시 광범위 영향 | JWKS 캐시, 키 로테이션 실패 영향 |
| 멀티 서비스 | 공용 세션 공유/동기화 필요 | audience/issuer 분리, 각 서비스가 검증 |
주의점
- 즉시 회수, 세밀한 세션 관리가 매우 중요한 관리자 콘솔 등은 세션이 더 간편하다!
- 민감 PII를 많이 담아야 하면 토큰에 포함하지 말거나 JWS→JWE(중첩) 로 기밀성 보장
- 리프레시 토큰은 대개 불투명 이 운영상 유리하다.(서버에서 회수·재사용 감지 용이)
- 브라우저 저장소: HttpOnly + SameSite 쿠키 권장(헤더 전송은 CSRF에 강하지만 XSS 취약)
- 반드시 HTTPS, 토큰 쿼리스트링 금지, 로그에 토큰/클레임 금지
참고
https://datatracker.ietf.org/doc/html/rfc7519
RFC 7519: JSON Web Token (JWT)
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JS
datatracker.ietf.org
'Computer Science > Server' 카테고리의 다른 글
| 도메인 주도 개발 (0) | 2025.11.27 |
|---|---|
| 서버란? (0) | 2025.08.27 |