JSON Web Token
1. JWT란?
JSON 포맷을 이용해 정보를 안전하게 전달하기 위한 토큰 기반 인증 방식입니다.
주로 서버와 클라이언트 간 인증 정보를 주고받을 때 사용됩니다.
2. JWT를 쓰는 이유이자 강점
- 무상태(Stateless) 인증 가능
- 무상태란? 클라이언트에서 보낸 데이터를 서버 측에서 따로 저장하지 않는다는 소리
- 서버에서는 JWT 암호를 복호화하는 작업을 하여 데이터를 추출
- 데이터가 토큰 안에 있어 DB 조회 x -> 빠른 인증 처리
- Restful Api에 적합: 무상태성을 유지하는 API에 유리
- 확장성 좋음
- 서버가 여러 대여도 서명(signature)를 알고 있으면 다른 서버에서도 복호화하여 사용가능
- 자체 검증 가능
- 토큰에 서명이 포함되어 있어 위변조 여부 쉽게 검사 가능
- 서명은 탈취 절대 불가능하게 할 것!
- 다양한 클라이언트 환경 지원
- 웹, 모바일, API 등 여러 환경에서 동일하게 사용 가능
- 다양한 클레임 포함 가능: 사용자 정보, 권한, 만료시간 등 넣을 수 있음
3. 약점
- 토큰 탈취 위험: 탈취되면 만료 전까지 악용 가능
- 토큰 무효화 어려움: 서버가 상태를 관리하지 않아 특정 토큰을 강제로 만료시키기 힘듦
- 토큰 크기 큼: 쿠키나 헤더에 많이 쓰면 네트워크 부담 증가
- 보안 주의 필요: HTTPS 사용 필수, 서명 키 관리 중요
- 갱신 관리 번거로움: 보통 리프레시 토큰 별도 관리 필요
그럼 이 약점들을 어떻게 극복해야할까?
- 토큰 탈취 위험 & 보안 강화
- HTTPS 사용 필수
- HttpOnly, Secure 쿠키 사용
- JWT를 클라이언트 저장소(LocalStorage 등)가 아니라, 브라우저 쿠키에 HttpOnly 및 Secure 옵션을 줘서 자바스크립트로 접근 불가능하게 만들면 XSS 공격 위험 감소.
- 짧은 액세스 토큰 수명
- 액세스 토큰 만료 시간을 짧게 설정해서 탈취 후 악용 가능 시간을 최소화.
- 토큰 무효화 어려움 & 갱신 관리
-
리프레시 토큰 사용
- 위에서 엑세스 토큰 수명을 짧게 하라고 했다. 그런데 매번 인증하려고 유저가 로그인을 계속해야한다면? UX적으로 최악일 것이다.
- 그렇기 때문에 액세스 토큰과 별개로 상대적으로 만료 기간이 긴 리프레시 토큰을 발급하고, 액세스 토큰이 만료되면 리프레시 토큰으로 재발급 받게 함.
- 리프레시 토큰은 서버에서 관리(저장 및 검증)해 강제 만료 가능.
-
블랙리스트(Blacklist) 구현
- 탈취된 토큰이나 로그아웃된 토큰을 서버에서 별도 저장소에 기록해서, 토큰 검증 시 블랙리스트에 있으면 거부.
- 다만 서버 상태 관리가 필요해서 무상태 JWT의 장점이 줄어들 수 있으므로 일반적인 RDB에 저장하는 것보단 Redis같은 곳에 저장해보는것도 좋을 것 같다.
-
토큰 버전 관리
- 유저 데이터에 토큰 버전을 저장해, 서버에서 현재 토큰 버전과 일치하지 않으면 무효 처리하는 방법.
- 토큰 크기 문제 완화
- 페이로드 최소화
- 토큰에 꼭 필요한 정보만 담고, 불필요한 클레임은 제거해서 크기 축소.
- 압축(Compression) 적용
- JWT에 압축 알고리즘을 적용하는 방법도 있으나, 구현 복잡도가 올라감.
- 기타 보안 강화
- 서명 키 주기적 교체
- 비밀 키를 주기적으로 변경해서 오래된 키로 서명된 토큰을 무효화.
- CORS 정책 강화
- API 서버에 엄격한 CORS 설정으로 권한 없는 출처에서의 요청 차단.
4. JWT 외에 인증 방식
- 세션 기반 인증 (Session + Cookie)
- 서버가 세션 상태를 메모리/DB에 저장하고, 클라이언트는 세션 ID를 쿠키로 보냄
→ 서버가 상태를 유지해야 해서 확장성이 떨어짐
- OAuth / OAuth2
- 주로 제3자 인증 시 사용, 복잡하지만 권한 위임에 강점
- API Key
- Basic Auth
- 사용자명/비밀번호를 매 요청마다 전송, 보안에 취약함
5. JWT vs session 기반 인증
| 구분 | JWT | 세션 기반 인증 |
|---|
| 상태 | 무상태 (stateless) | 상태 유지 (stateful) |
| 서버 부담 | 낮음 | 세션 저장으로 부하 존재 |
| 확장성 | 매우 좋음 | 확장에 불리함 |
| 보안 | 토큰 탈취 시 위험, 무효화 어려움 | 서버에서 세션 만료 관리 가능 |
| 사용 환경 | SPA, 모바일, 마이크로서비스, API 인증 등에 주로 사용 | 전통적인 웹사이트 로그인, 서버 렌더링 기반 앱에 적합 |
6. JWT의 구조
- alg: 토큰을 서명하는데 사용할 알고리즘 종류 (예: HS256, RS256 등)
- typ: 토큰 타입 (보통 "JWT"라고 명시)
{
"alg": "HS256",
"typ": "JWT"
}
Payload
- 실제 데이터(Claims)가 담기는 부분.
- 등록된 클레임(Registered Claims)
- JWT 표준에서 미리 정의한 예약된 키들이에요.
주로 토큰 발급자, 만료시간, 대상자 등 토큰의 기본 정보입니다.
{
// 등록된 클레임 예시
"iss": "auth.myapp.com", // 토큰 발급자 (issuer)
"sub": "user123", // 토큰 주제 (subject), 주로 사용자 ID
"aud": "myapp.com", // 토큰 대상자 (audience)
"exp": 1700000000, // 토큰 만료시간 (expiration, Unix timestamp)
"nbf": 1699990000, // 토큰 사용 가능 시작 시간 (not before)
"iat": 1699980000, // 토큰 발행 시간 (issued at)
"jti": "unique-token-id-123" // JWT 고유 ID (JWT ID)
}
-
공개 클레임(Public Claims)
-
비공개 클레임(Private Claims)
- 발급자와 소비자(서버, 클라이언트) 간에 약속된 임의의 키-값 쌍이에요.
{
"userId": "123456",
"isPremiumUser": true,
"department": "engineering"
}
모든 클레임을 다 쓰면 이런 모습입니다.
{
"iss": "auth.myapp.com",
"sub": "user123",
"aud": "myapp.com",
"exp": 1700000000,
"iat": 1699980000,
"https://myapp.com/roles": ["admin", "editor"],
"userId": "123456",
"isPremiumUser": true
}
허나 다 쓸 필요 없습니다. 필요하다 느끼는 것만 적절히 쓰면 좋습니다.
Signature (서명)
- 서버가 Header와 Payload를 비밀키(secret key)를 이용해 서명한 값이에요.
- 서명을 만드는 과정 (예: HMAC SHA-256 알고리즘):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
7. JWT 생성 & 검증 흐름
- 서버가 Header와 Payload를 만들어서 Base64Url로 인코딩
- 인코딩된 Header와 Payload를 합쳐서 비밀키로 서명
- HEADER.PAYLOAD.SIGNATURE 형태의 토큰 발급
- 클라이언트가 토큰을 저장하고 요청 시마다 전달
- 서버가 토큰의 서명을 검증하고, Payload를 읽어 인증 처리
정리
| 부분 | 내용 | 역할 |
|---|
| Header | 토큰 타입, 서명 알고리즘 정보 | 서명 만들 때 사용되는 메타정보 |
| Payload | 사용자 정보, 권한, 만료 시간 등 | 실제 인증/인가에 필요한 데이터 |
| Signature | Header와 Payload를 비밀키로 서명 | 토큰 변조 방지 및 신뢰성 확보 |