[Daily morning study] JWT vs Session 인증 방식 비교

#daily morning study

Image


인증(Authentication)이란

인증은 클라이언트가 자신이 누구인지 서버에 증명하는 과정이다. HTTP는 기본적으로 무상태(stateless) 프로토콜이라 요청마다 클라이언트를 식별할 수단이 필요하다. 그 수단으로 크게 두 가지가 쓰인다: Session 방식과 JWT 방식.


Session 기반 인증

동작 흐름

  1. 사용자가 아이디/비밀번호로 로그인 요청
  2. 서버가 자격증명을 검증하고 세션을 생성, 세션 저장소(메모리, Redis 등)에 저장
  3. 서버는 응답에 Set-Cookie: sessionId=<id> 헤더 포함
  4. 이후 클라이언트는 매 요청마다 쿠키에 세션 ID를 담아 전송
  5. 서버는 세션 저장소에서 세션 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에 넣으면 안 된다.

동작 흐름

  1. 사용자가 로그인 요청
  2. 서버가 검증 후 JWT를 생성해 클라이언트에 반환
  3. 클라이언트가 토큰을 저장(localStorage, sessionStorage, 쿠키 등)
  4. 이후 요청마다 Authorization: Bearer <token> 헤더에 토큰 포함
  5. 서버는 서명을 검증해 토큰의 유효성을 확인 — 저장소 조회 없음
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의 의미

서버는 토큰을 저장하지 않는다. 검증은 서명만으로 이루어진다. 같은 비밀키를 공유하는 서버라면 어떤 인스턴스도 토큰을 검증할 수 있다.


비교

항목SessionJWT
상태 관리서버(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에 담는 클레임에는 표준 클레임이 있다.

클레임의미
issIssuer: 토큰 발급자
subSubject: 토큰 대상(보통 사용자 ID)
audAudience: 토큰 수신자
expExpiration: 만료 시간(Unix timestamp)
iatIssued At: 발급 시간
jtiJWT ID: 토큰 고유 식별자(중복 방지)

표준 클레임 외에 커스텀 클레임(role, email 등)을 추가할 수 있다.