icon

안동민 개발노트

9장 : HTTP

쿠키와 세션


HTTP는 무상태(Stateless) 프로토콜입니다. 서버는 각 요청을 독립적으로 처리하며, 이전 요청과의 연관성을 기억하지 않습니다. 하지만 로그인 상태를 유지하거나, 장바구니에 담은 상품을 기억하려면 상태가 필요합니다.

이 모순을 해결하기 위해 만들어진 메커니즘이 쿠키(Cookie)입니다.

HTTP 무상태의 문제
무상태라면
  요청1: POST /login (id=hong, pw=1234)  → "로그인 성공!"
  요청2: GET /my-profile                 → "누구세요?" (기억 못함)
  요청3: POST /buy (item=책)             → "누구세요?" (기억 못함)

  서버 입장에서 요청1과 요청2는 전혀 관련 없는 요청
  → 상태를 유지할 방법이 필요!

쿠키의 동작 원리

쿠키는 서버가 클라이언트(브라우저)에 저장하도록 보내는 작은 데이터 조각입니다.

쿠키 기반 상태 관리
1. 로그인 요청
  Client → Server: POST /login {id: "hong", pw: "1234"}
  Server → Client: 200 OK
                   Set-Cookie: session_id=abc123; HttpOnly; Secure

2. 이후 모든 요청 (브라우저가 자동 포함)
  Client → Server: GET /my-profile
                   Cookie: session_id=abc123
  Server: "abc123은... 홍길동이구나!" → 프로필 반환

3. 장바구니
  Client → Server: POST /cart/add
                   Cookie: session_id=abc123
  Server: "홍길동의 장바구니에 추가!"

핵심: 서버는 여전히 무상태
      쿠키가 매 요청에 "나는 홍길동이야"를 증명

쿠키 속성

쿠키에는 동작 방식을 제어하는 여러 속성이 있습니다. 보안과 직결되므로 정확히 이해해야 합니다.

속성값 예시기능보안 영향
Domain.example.com유효 도메인서브도메인 포함 여부
Path/api유효 경로특정 경로만 전송
Expires날짜만료 시점지정 안 하면 세션 쿠키
Max-Age3600유효 기간(초)Expires보다 우선
HttpOnly(플래그)JS 접근 차단XSS 방어 필수
Secure(플래그)HTTPS만 전송감청 방어 필수
SameSiteStrict/Lax/None크로스사이트 제한CSRF 방어
SameSite 속성 상세
사용자가 bank.com에 로그인한 상태에서
evil.com을 방문하면?

evil.com의 HTML
  <form action="https://bank.com/transfer" method="POST">
    <input name="to" value="hacker">
    <input name="amount" value="1000000">
  </form>
  <script>document.forms[0].submit()</script>

→ 브라우저가 bank.com에 POST 요청을 보냄
→ Cookie: session_id=abc123 자동 포함!
→ CSRF(Cross-Site Request Forgery) 공격!

SameSite=Strict
  다른 사이트에서 온 모든 요청에 쿠키 미포함
  → CSRF 완전 차단
  → 단, 외부 링크로 bank.com 접속해도 쿠키 없음 (불편)

SameSite=Lax (기본값)
  탑 레벨 네비게이션(링크 클릭, GET)만 쿠키 포함
  다른 사이트의 POST/iframe 등에는 미포함
  → CSRF 대부분 차단, 사용성 유지

SameSite=None
  모든 크로스사이트 요청에 쿠키 포함
  → Secure 필수 (HTTPS만)
  → 서드파티 쿠키에 사용
안전한 쿠키 설정 예시
Set-Cookie: session_id=abc123;
  HttpOnly;          ← JS에서 document.cookie 접근 불가 (XSS 방어)
  Secure;            ← HTTPS에서만 전송 (감청 방어)
  SameSite=Lax;      ← 크로스사이트 POST 차단 (CSRF 방어)
  Path=/;            ← 전체 경로에서 유효
  Max-Age=86400;     ← 24시간 후 만료

세션 기반 인증 vs 토큰 기반 인증

쿠키를 활용한 인증 방식은 크게 두 가지입니다.

세션 기반 인증
                  서버 (세션 저장소)
                  ┌───────────────────┐
Client ──Cookie──→│ session_id        │
  session_id=abc  │  abc → {          │
                  │   user: "홍길동", │
                  │   role: "admin"   │
                  │  }                │
                  │  def → {...}      │
                  │  ghi → {...}      │
                  └───────────────────┘

장점
  * 서버가 세션 통제 가능 (강제 로그아웃 = 세션 삭제)
  * 민감 정보가 서버에만 존재 (클라이언트는 ID만 가짐)
  * 세션 데이터 크기 제한 없음

단점
  * 서버 메모리/저장소 필요 (사용자 100만 → 100만 세션)
  * 서버 확장 시 세션 공유 문제
    ┌──Server A──┐  세션 abc 있음
    │            │
    ┌──Server B──┐  세션 abc 없음! → 로그인 풀림
    해결: Redis 같은 중앙 세션 저장소 사용
토큰 기반 인증 (JWT)
JWT 구조
  Header.Payload.Signature
  Header
  ┌──────────┐
  │{"alg":   │
  │ "HS256"} │
  └──────────┘

  Payload
  ┌──────────────────┐
  │{"sub":"홍길동",  │
  │ "role":"admin",  │
  │ "exp":1700000000}│
  └──────────────────┘

  Signature
  ┌──────────┐
  │HMACSHA256│
  │(header + │
  │ payload, │
  │ secret)  │
  └──────────┘

  Base64URL 인코딩 → eyJhbGci.eyJzdWI.SflKxw...

동작
  Client ──Authorization: Bearer eyJhbGci...──→ Server
  Server
    1. 서명 검증 (비밀키로) → 변조 확인
    2. 만료 시간 확인      → 유효 기간
    3. 페이로드에서 사용자 정보 추출
    → 세션 저장소 조회 불필요!

장점
  * 서버 무상태 (저장소 불필요) → 수평 확장 용이
  * 서버 B도 같은 비밀키면 검증 가능

단점
  * 발행 후 무효화 어려움 (토큰 탈취 시 만료까지 유효)
  * 페이로드 크기가 쿠키 크기 제한에 걸림
  * 민감 정보 포함 시 디코딩 가능 (암호화 아님!)
비교 항목세션 기반토큰 기반 (JWT)
서버 저장소필요 (Redis 등)불필요
확장성세션 공유 필요뛰어남
강제 로그아웃세션 삭제로 즉시 가능블랙리스트 필요 (어려움)
보안서버 관리토큰 탈취 위험
크기세션 ID만 (작음)페이로드 포함 (큼)
마이크로서비스서비스간 세션 공유 어려움토큰 전달로 간편

서드파티 쿠키와 프라이버시

서드파티 쿠키 추적 원리
사용자가 shopping.com 방문
  ┌─ shopping.com ───────────────┐
  │ 상품 목록                    │
  │ ┌──────────────────────────┐ │
  │ │ <img src="ad.tracker.com │ │ ← 서드파티 리소스
  │ │  /pixel?page=shopping">  │ │
  │ └──────────────────────────┘ │
  └──────────────────────────────┘
  ad.tracker.com → Set-Cookie: uid=user123

사용자가 news.com 방문
  ┌─ news.com ───────────────────┐
  │ 뉴스 기사                    │
  │ ┌──────────────────────────┐ │
  │ │ <img src="ad.tracker.com │ │ ← 같은 서드파티
  │ │  /pixel?page=news">      │ │
  │ └──────────────────────────┘ │
  └──────────────────────────────┘
  Cookie: uid=user123 자동 전송!
  → ad.tracker.com: "user123이 shopping.com 다음에 news.com 방문했구나"
  → 맞춤 광고 제공

브라우저 대응
  Safari: ITP(Intelligent Tracking Prevention) → 서드파티 쿠키 차단
  Firefox: ETP(Enhanced Tracking Protection) → 추적기 쿠키 차단
  Chrome: Privacy Sandbox, 서드파티 쿠키 제한 정책
cookie_demo.py
from http.server import HTTPServer, BaseHTTPRequestHandler
from http.cookies import SimpleCookie
from datetime import datetime

class CookieHandler(BaseHTTPRequestHandler):
    """쿠키 동작 데모 서버"""
    
    def do_GET(self):
        cookies = SimpleCookie(self.headers.get("Cookie", ""))
        visit_count = int(cookies.get("visits", type("", (), {"value": "0"})).value)
        visit_count += 1
        
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        
        # 안전한 쿠키 설정
        self.send_header(
            "Set-Cookie",
            f"visits={visit_count}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400"
        )
        
        self.end_headers()
        
        html = f"""
        <h1>쿠키 데모</h1>
        <p>방문 횟수: {visit_count}</p>
        <p>현재 시각: {datetime.now()}</p>
        <p>수신된 쿠키 헤더: {self.headers.get('Cookie', '없음')}</p>
        """
        self.wfile.write(html.encode())

# HTTPServer(("localhost", 8080), CookieHandler).serve_forever()

다음 절에서는 HTTP의 캐시 전략과 크로스 오리진 요청을 다루는 CORS를 살펴보겠습니다.

목차