0) 세션이 왜 필요했을까? (세션이 없으면 생기는 문제)

세션(또는 토큰) 같은 “로그인 상태 유지 장치”가 없다면, 서버는 요청을 받을 때마다 사용자가 누군지 알 방법이 없습니다.
그러면 로그인 이후에도 다음 같은 일이 생겨요:

  • 사용자가 장바구니 담기 버튼을 누름 → 서버는 “누가 눌렀는지” 몰라서 다시 로그인 요구
  • 마이페이지, 주문내역, 좋아요 같은 “내 정보”가 필요한 페이지를 누를 때마다 로그인 요구
  • 심지어 “로그인 성공” 직후에 페이지 이동(GET /me)만 해도, 서버는 다시 “누구세요?”가 됨

즉, “로그인 한 번으로 여러 요청에서 동일 사용자를 이어서 식별”하려면
브라우저가 매 요청마다 뭔가를 같이 보내줘야 하고(쿠키/헤더), 서버도 그걸 해석할 규칙이 필요합니다.


1) Session 개념

세션 방식은 보통 이렇게 나뉩니다.

  • 서버: “로그인 상태(사용자 정보/권한 등)”를 세션 저장소(메모리/Redis/DB)에 저장
  • 클라이언트(브라우저): “세션을 가리키는 값”인 세션ID(Session ID) 를 쿠키로 저장하고, 매 요청마다 자동 전송

쿠키는 서버가 Set-Cookie로 내려주고, 브라우저는 이후 요청에 Cookie 헤더로 자동 포함합니다.

서버가 로그인 성공 후 내려주는 응답 헤더 예시:

Set-Cookie: SESSIONID=9bb0422b85e7acb50d25d57147310672; Path=/; HttpOnly; Secure; SameSite=Lax

이후 브라우저 요청 예시:

Cookie: SESSIONID=9bb0422b85e7acb50d25d57147310672

서버는 세션ID로 세션 저장소를 조회해요. 예를 들어 Redis/DB에 이런 형태로 저장될 수 있습니다:

{
  "sessionId": "9bb0422b85e7acb50d25d57147310672",
  "userId": 123,
  "role": "user",
  "createdAt": "2026-01-05T09:12:33+09:00",
  "lastSeenAt": "2026-01-05T09:25:10+09:00"
}

2) Session를 통한 로그인 방법

아래는 세션 로그인 전체 흐름입니다.

sequenceDiagram
autonumber
participant C as Client
participant S as Server
participant SS as SessionStore

C->>S: POST /login
S->>S: Verify credentials
S->>SS: Create sessionId and save userId role
SS-->>S: Saved
S-->>C: Set-Cookie SESSIONID=sessionId

C->>S: Request with Cookie SESSIONID
S->>SS: Lookup session by sessionId
SS-->>S: session data userId role
S-->>C: Authorized response

순서별 설명

  1. Client가 로그인 요청을 보냅니다.
  2. Server가 아이디/비밀번호를 검증합니다.
  3. ServersessionId를 만들고, 사용자 정보(userId, role)를 세션 저장소에 저장합니다.
  4. SessionStore가 저장 완료를 응답합니다.
  5. ServerSet-Cookie: SESSIONID=sessionId로 세션ID를 내려줍니다.
  6. 이후 Client는 요청마다 Cookie: SESSIONID=...를 자동으로 포함해 보냅니다.
  7. Server는 쿠키의 SESSIONID로 세션 저장소에서 세션을 조회합니다.
  8. SessionStore가 세션 데이터(예: userId, role)를 반환합니다.
  9. Server는 조회 결과를 바탕으로 인가 후 응답합니다.

“양념 예시”: 주문조회 API를 호출할 때 실제로는?

  • 요청: GET /orders + Cookie: SESSIONID=9bb0422b85e7acb50d25d57147310672
  • 서버: SESSIONID=9bb0422b85e7acb50d25d57147310672로 세션 조회 → userId=123 확인
  • userId=123의 주문만 조회해서 응답

3) Session 방법의 한계

3.1 확장성(Scale-out) 부담

서버가 여러 대면, 세션이 어느 서버/어느 저장소에 있느냐가 문제가 됩니다.
그래서 sticky session 또는 중앙 세션 저장소(예: Redis)가 필요해지곤 합니다.

3.2 세션ID 보호가 곧 보안

세션 방식에서 진짜 “열쇠”는 세션ID입니다.
SESSIONID가 탈취되면, 공격자는 그 세션으로 로그인된 사용자처럼 행동할 수 있어요.

그래서 쿠키는 보통 아래 옵션을 고려합니다(브라우저 보안 속성):

  • HttpOnly: JS에서 쿠키 접근 제한(일부 XSS 피해 완화)
  • Secure: HTTPS에서만 전송
  • SameSite: 크로스 사이트 자동 전송 제한(일부 CSRF 완화)

“양념 예시”: 세션 방식 로그아웃이 쉬운 이유

세션 방식 로그아웃은 보통 이렇게 끝납니다.

  1. 서버가 9bb0422b85e7acb50d25d57147310672 세션 레코드를 삭제
  2. 브라우저 쿠키도 만료(또는 삭제) 처리

→ “서버가 상태를 들고 있기에” 강제 로그아웃/차단이 쉽습니다.


4) JWT 등장, 개념

JWT는 클레임(Claim) 을 JSON으로 담아, 토큰 하나로 들고 다니는 형식입니다.

JWT 문자열은 보통 이렇게 생겼습니다:

header.payload.signature
  • header: 토큰 타입, 서명 알고리즘 등 메타 정보
  • payload: Claim (예: sub, role, iat, exp …)
  • signature: base64url(header) + "." + base64url(payload) 를 비밀키로 서명한 값(무결성 검증)

중요한 포인트: payload는 “암호화”가 아니라 Base64URL 인코딩입니다.
누구나 디코딩해서 내용을 볼 수 있어요 → 민감정보를 넣으면 안 됩니다.

4.1 실제 JWT 예시 (진짜처럼 생긴 값)

아래는 학습용으로 만든 HS256(HMAC-SHA256) 서명 JWT 예시입니다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9.7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM

이 토큰을 . 기준으로 나누면:

  • header(1번째 조각) = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • payload(2번째 조각) = eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9
  • signature(3번째 조각) = 7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM

4.2 header 디코딩 예시

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 를 Base64URL 디코딩하면:

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

여기서 핵심은:

  • typ: JWT
  • alg: HS256 (대칭키 HMAC-SHA256)

4.3 payload(Claim) 디코딩 예시

eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9 를 Base64URL 디코딩하면 Claim이 나옵니다:

{
  "sub": "u123",
  "role": "admin",
  "iat": 1700000000,
  "exp": 1700000600
}

예시 Claim 의미:

  • sub: 사용자 식별자(Subject) → "u123"
  • role: 권한 → "admin"
  • iat: 발급 시각
  • exp: 만료 시각(이 시간이 지나면 거부)

5) JWT를 통한 로그인 방법

JWT 기반 로그인/인가 흐름은 보통 아래처럼 갑니다.

sequenceDiagram
autonumber
participant C as Client
participant A as AuthServer
participant R as ResourceServer

C->>A: POST /login
A->>A: Verify credentials
A-->>C: Issue access token JWT

C->>R: Request with Authorization Bearer JWT
R->>R: Verify signature and claims exp
R-->>C: Authorized response

순서별 설명

  1. Client가 로그인 요청을 보냅니다.
  2. AuthServer가 아이디/비밀번호를 검증합니다.
  3. 검증이 성공하면 AuthServerAccess Token(JWT) 을 발급해서 내려줍니다.
  4. 이후 Client는 API 호출마다 Authorization: Bearer <JWT>로 토큰을 전송합니다.
  5. ResourceServer는 토큰의 서명(signature)클레임(exp 등) 을 검증합니다.
  6. 검증이 통과되면 인가 후 응답합니다.

“양념 예시”: 실제 요청 헤더는 이렇게 보임

GET /me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9.7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM

6) JWT 서명 검증을 “실제 값”으로 이해하기

서버는 내부적으로 이런 검증을 합니다(개념):

  1. 클라이언트가 보낸 토큰에서 headerpayload를 그대로 떼어냄
  2. 서버가 가진 비밀키(secret)signature다시 계산
  3. 계산 결과가 토큰의 signature와 같으면 “변조 없음”으로 판단
  4. 그리고 exp(만료), 필요하면 iss/aud 같은 claim도 추가로 검증

“양념 예시”: payload만 바꿔치기하면?

공격자가 payload에서 role을 바꾸고(signature는 그대로 둠):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwicm9sZSI6InN1cGVyYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMDYwMH0.7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM
  • 토큰 자체는 “그럴듯”하게 보이지만
  • 서버가 signature를 재계산하면 불일치 → 거부되어야 정상입니다.

7) JWT 방법의 한계

7.1 강제 로그아웃/폐기가 세션보다 어렵다

세션은 서버가 지우면 끝인데, JWT는 클라이언트가 들고 있으면 exp 만료 전까지 유효할 수 있습니다.
즉 “로그아웃했는데, 토큰이 유출된 공격자는 만료 전까지 호출” 같은 문제가 생길 수 있어요.

7.2 Bearer 토큰 특성

Bearer 토큰은 “가진 사람이 주인”처럼 동작하기 쉬워서 유출 시 위험합니다.
그래서 HTTPS 강제, 저장소 선택(쿠키/스토리지), 만료 짧게 등 설계가 중요해집니다.


8) JWT 보완: Access Token / Refresh Token

실무에서는 보통 2-토큰 패턴을 씁니다.

  • Access Token: 짧게(예: 5~15분) → API 호출용
  • Refresh Token: 길게(예: 며칠~몇 주) → Access Token 재발급용

아래는 흐름입니다.

sequenceDiagram
autonumber
participant C as Client
participant A as AuthServer
participant R as ResourceServer

C->>A: Login success
A-->>C: access token short
A-->>C: refresh token long

C->>R: API call with access token
R->>R: Verify access token
R-->>C: Response

C->>R: API call with expired access token
R-->>C: 401 token expired

C->>A: POST /token with refresh token
A->>A: Validate refresh token
A-->>C: New access token

순서별 설명

  1. 로그인에 성공하면 AuthServeraccess token(짧은 수명)과 refresh token(긴 수명)을 내려줍니다.
  2. Client는 API 호출에 access token을 사용합니다.
  3. ResourceServer는 access token을 검증하고 정상 응답합니다.
  4. 시간이 지나 access token이 만료되면, 동일 호출이 실패할 수 있습니다.
  5. 서버는 만료를 감지하고(예: 401) 재발급이 필요하다는 신호를 줍니다.
  6. Clientrefresh token으로 토큰 엔드포인트(/token)에 재발급 요청을 보냅니다.
  7. AuthServer가 refresh token을 검증합니다.
  8. 검증이 통과되면 새 access token을 발급해서 내려줍니다.

“양념 예시”: refresh token은 보통 의미 없는 랜덤 문자열(opaque)

예시:

rt_7FjQLL58aHUUMlKj-GcxbmO1ENz6rshgp8JDg11XbMg

그리고 서버는 refresh token을 DB/Redis에 저장하고(폐기/회전/재사용 감지),
클라이언트가 access token 만료 시에만 재발급에 사용하게 만듭니다.


9) 참고 자료