[Daily morning study] JWT vs Session 인증 방식 비교
#daily morning study
인증(Authentication)이란
인증은 클라이언트가 자신이 누구인지 서버에 증명하는 과정이다. HTTP는 기본적으로 무상태(stateless) 프로토콜이라 요청마다 클라이언트를 식별할 수단이 필요하다. 그 수단으로 크게 두 가지가 쓰인다: Session 방식과 JWT 방식.
Session 기반 인증
동작 흐름
- 사용자가 아이디/비밀번호로 로그인 요청
- 서버가 자격증명을 검증하고 세션을 생성, 세션 저장소(메모리, Redis 등)에 저장
- 서버는 응답에
Set-Cookie: sessionId=<id>헤더 포함 - 이후 클라이언트는 매 요청마다 쿠키에 세션 ID를 담아 전송
- 서버는 세션 저장소에서 세션 ID를 조회해 사용자를 식별
[Client] [Server] [Session Store]
|--- POST /login -------->| |
| |--- store session ----------->|
|<-- Set-Cookie: sid=xxx --| |
| | |
|--- GET /profile ------->| |
| Cookie: sid=xxx |--- lookup sid=xxx ---------->|
| |<-- user data ----------------|
|<-- 200 user data --------| |
특징
- 세션 상태를 서버가 관리한다. 로그아웃 시 서버 측에서 세션을 삭제하면 즉시 무효화된다.
- 세션 ID 자체에는 사용자 정보가 없으므로, 탈취되더라도 세션 만료나 서버 삭제로 대응 가능하다.
- 서버가 상태를 보관해야 하므로, 서버가 여러 대일 때는 세션 공유 문제가 생긴다.
단점: 분산 환경에서의 세션 공유 문제
┌─────────────┐
│ Load Balancer│
└──────┬──────┘
┌───────┴───────┐
┌────▼────┐ ┌────▼────┐
│ Server1 │ │ Server2 │
│ session │ │ session │
│ A,B,C │ │ D,E,F │
└─────────┘ └─────────┘
# 사용자가 Server1에서 로그인한 세션이
# Server2로 라우팅되면 세션을 찾을 수 없다
이를 해결하기 위해 Redis 같은 중앙화된 세션 저장소를 사용하거나, 스티키 세션(Sticky Session)을 적용한다.
JWT(JSON Web Token) 기반 인증
JWT 구조
JWT는 .으로 구분된 세 부분으로 이루어진다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzI5NjAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| 파트 | 내용 | 예시 |
|---|---|---|
| Header | 알고리즘, 토큰 타입 | {"alg": "HS256", "typ": "JWT"} |
| Payload | 클레임(사용자 정보, 만료시간 등) | {"sub": "user123", "exp": 1729600000} |
| Signature | 헤더+페이로드를 비밀키로 서명한 값 | HMAC-SHA256 등 |
각 파트는 Base64URL로 인코딩된다. Payload는 암호화가 아닌 인코딩이므로 누구나 디코딩해서 볼 수 있다. 민감한 정보는 Payload에 넣으면 안 된다.
동작 흐름
- 사용자가 로그인 요청
- 서버가 검증 후 JWT를 생성해 클라이언트에 반환
- 클라이언트가 토큰을 저장(localStorage, sessionStorage, 쿠키 등)
- 이후 요청마다
Authorization: Bearer <token>헤더에 토큰 포함 - 서버는 서명을 검증해 토큰의 유효성을 확인 — 저장소 조회 없음
import jwt
SECRET = "my-secret-key"
# 토큰 생성
payload = {"sub": "user123", "role": "admin", "exp": 1729600000}
token = jwt.encode(payload, SECRET, algorithm="HS256")
# 토큰 검증
decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
print(decoded) # {"sub": "user123", "role": "admin", "exp": 1729600000}
Stateless의 의미
서버는 토큰을 저장하지 않는다. 검증은 서명만으로 이루어진다. 같은 비밀키를 공유하는 서버라면 어떤 인스턴스도 토큰을 검증할 수 있다.
비교
| 항목 | Session | JWT |
|---|---|---|
| 상태 관리 | 서버(Stateful) | 클라이언트(Stateless) |
| 저장 위치 | 서버 메모리/DB | 클라이언트 |
| 무효화 | 즉시 가능 | 만료 전까지 어려움 |
| 확장성 | 세션 공유 필요 | 수평 확장 용이 |
| 페이로드 크기 | 세션 ID만 전송(작음) | 토큰 자체 전송(상대적으로 큼) |
| 보안 | 세션 ID 탈취 시 서버 삭제 가능 | 서명 검증만 가능, 탈취 대응 제한 |
JWT의 토큰 무효화 문제
JWT의 핵심 단점은 만료 전에 토큰을 무효화하기 어렵다는 점이다. 비밀키가 바뀌지 않는 한 서명이 유효하다.
해결책: Refresh Token + Access Token 패턴
Access Token: 수명 짧게(5~15분), 매 요청마다 사용
Refresh Token: 수명 길게(7~30일), 서버 DB에 저장
[Client] [Server]
|--- POST /login ----------->|
|<-- access_token(15m) -----|
| refresh_token(7d) |
| |
|--- GET /api (access_token)->|
|<-- 200 data ----------------|
| |
# access_token 만료
|--- POST /token/refresh ---->|
| (refresh_token) |-- DB에서 refresh_token 검증
|<-- new access_token --------|
Refresh Token을 DB에 저장하면, 로그아웃 시 DB에서 삭제해 무효화할 수 있다. Access Token은 수명이 짧아 탈취 피해를 최소화한다.
블랙리스트 방식
Access Token을 즉시 무효화해야 할 때는 Redis 등에 블랙리스트를 관리한다.
# 로그아웃 시 토큰을 블랙리스트에 추가
redis_client.setex(f"blacklist:{token}", ttl=token_remaining_ttl, value=1)
# 요청 검증 시
def verify_token(token):
if redis_client.exists(f"blacklist:{token}"):
raise Exception("Token revoked")
return jwt.decode(token, SECRET, algorithms=["HS256"])
단, 이 방식은 결국 서버 저장소를 사용하므로 Stateless의 이점이 줄어든다.
저장 위치에 따른 보안 고려사항
localStorage
// XSS 공격에 취약 — 악성 스크립트가 토큰을 탈취 가능
localStorage.setItem("token", jwtToken);
httpOnly 쿠키
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict
- HttpOnly: JavaScript에서 접근 불가 → XSS 방어
- Secure: HTTPS에서만 전송
- SameSite=Strict: CSRF 방어
httpOnly 쿠키에 JWT를 저장하면 XSS를 막을 수 있지만, CSRF 공격에는 별도 대응이 필요하다.
언제 무엇을 쓸까
Session이 유리한 경우
- 즉각적인 로그아웃/세션 무효화가 중요한 서비스(금융, 관리자 콘솔)
- 서버 인스턴스가 적고 단일 서버 구성
JWT가 유리한 경우
- 마이크로서비스 환경: 여러 서비스가 동일한 토큰을 공유 비밀키로 검증
- 모바일 앱처럼 쿠키 사용이 불편한 클라이언트
- 수평 확장이 잦은 stateless 서버 구성
클레임 종류
JWT Payload에 담는 클레임에는 표준 클레임이 있다.
| 클레임 | 의미 |
|---|---|
iss | Issuer: 토큰 발급자 |
sub | Subject: 토큰 대상(보통 사용자 ID) |
aud | Audience: 토큰 수신자 |
exp | Expiration: 만료 시간(Unix timestamp) |
iat | Issued At: 발급 시간 |
jti | JWT ID: 토큰 고유 식별자(중복 방지) |
표준 클레임 외에 커스텀 클레임(role, email 등)을 추가할 수 있다.