쿠키와 세션
HTTP는 무상태(Stateless) 프로토콜입니다. 각 요청은 그 자체로 해석될 수 있어야 하고, 같은 TCP 연결을 탔는지 여부가 요청의 의미를 결정하지 않습니다. 이 성질 덕분에 HTTP는 단순하고 확장하기 쉽지만, 로그인 상태나 장바구니처럼 “이전 요청의 결과”가 필요한 웹 애플리케이션은 별도의 상태 관리 장치가 필요합니다.
쿠키는 이 간격을 메우기 위해 서버와 브라우저가 합의한 작은 상태 전달 메커니즘입니다. 서버는 Set-Cookie 응답 헤더로 이름, 값, 범위, 만료, 보안 속성을 보내고, 브라우저는 이후 요청에서 조건에 맞는 쿠키만 Cookie 요청 헤더에 실어 보냅니다.
쿠키의 동작 원리
쿠키 자체가 사용자를 “증명”하는 것은 아닙니다. 보통 브라우저는 예측하기 어려운 세션 식별자를 쿠키에 저장하고, 서버는 그 식별자를 세션 저장소의 사용자 상태와 매핑합니다. 즉, 쿠키는 상태 데이터 전체가 아니라 서버가 상태를 찾기 위한 열쇠인 경우가 많습니다.
브라우저는 쿠키를 보낼 때 Domain, Path, Secure, SameSite, 만료 시각 같은 조건을 함께 평가합니다. 그래서 쿠키 문제를 디버깅할 때는 “쿠키 값이 있는가”뿐 아니라 “현재 요청이 쿠키의 전송 조건을 만족하는가”를 같이 봐야 합니다.
쿠키 속성
쿠키 속성은 저장 위치, 전송 범위, 수명, 스크립트 접근 여부, 크로스사이트 전송 여부를 제어합니다. 특히 인증 쿠키는 값 자체보다 스코프와 전송 조건을 좁히는 것이 중요합니다.
| 속성 | 값 예시 | 의미 | 주의할 점 |
|---|---|---|---|
| Domain | example.com | 쿠키를 보낼 호스트 범위 | 생략하면 현재 호스트 전용에 가까운 동작 |
| Path | /api | 쿠키를 보낼 URL 경로 범위 | 보안 경계라기보다 전송 범위 필터 |
| Expires | HTTP date | 절대 만료 시각 | Max-Age가 있으면 보통 Max-Age가 우선 |
| Max-Age | 3600 | 상대 만료 시간(초) | 음수나 0이면 삭제 용도로 쓰임 |
| HttpOnly | 플래그 | JS의 document.cookie 접근 제한 | XSS 자체를 막지는 않지만 세션 탈취 피해를 줄임 |
| Secure | 플래그 | 보안 연결에서만 전송 | HTTPS 배포의 기본값으로 두는 편이 안전 |
| SameSite | Lax/Strict/None | 크로스사이트 요청 전송 제어 | CSRF 완전 대체가 아니라 방어층 중 하나 |
SameSite와 CSRF
CSRF는 공격자가 사용자의 브라우저로 피해 사이트에 요청을 보내게 만드는 공격입니다. 쿠키 기반 로그인은 브라우저가 쿠키를 자동으로 붙여 보내기 때문에, 서버가 요청의 의도와 출처를 별도로 확인하지 않으면 문제가 됩니다.
SameSite=Lax는 많은 크로스사이트 POST 경로를 줄여주지만, 안전하지 않은 GET 엔드포인트, 같은 registrable domain의 하위 도메인, 오래된 클라이언트, 클라이언트 측 CSRF 같은 경우까지 해결하지는 않습니다. 중요한 상태 변경 요청은 CSRF 토큰, Origin/Referer 검증, 안전한 메서드 설계를 함께 사용해야 합니다.
안전한 쿠키 설정 예시
인증 세션 쿠키는 가능한 한 값은 무작위 식별자로 두고, 브라우저가 그 값을 어디에 저장하고 언제 보내는지를 좁혀야 합니다. 아래 설정은 일반적인 웹 애플리케이션의 기본 출발점입니다.
Set-Cookie: __Host-session=K8x...; Path=/; Max-Age=86400; Secure; HttpOnly; SameSite=Lax__Host- 접두사는 Secure, Path=/, Domain 미지정을 요구하는 방향으로 동작해 쿠키 스코프를 호스트 단위로 좁히는 데 도움이 됩니다. 단, 모든 보안은 서버 검증과 함께 완성됩니다. 쿠키 속성만으로 권한 검사, 세션 만료, 재발급, 로그아웃 처리를 대신할 수는 없습니다.
세션 기반 인증 vs 토큰 기반 인증
세션 기반 인증은 서버가 상태를 보관하고, 브라우저는 세션 ID만 들고 다닙니다. 이 구조는 강제 로그아웃, 권한 변경 반영, 세션 만료 관리가 비교적 직접적입니다. 대신 여러 서버가 있는 환경에서는 세션 저장소를 공유하거나 sticky session, 중앙 저장소 같은 설계가 필요합니다.
토큰 기반 인증, 특히 JWT는 토큰 안에 claim을 담고 서명으로 무결성을 검증합니다. 이 덕분에 리소스 서버가 중앙 세션 저장소를 매번 조회하지 않고도 토큰을 검증할 수 있습니다. 하지만 토큰이 만료되기 전까지 살아 있다는 점은 운영상 부담이 됩니다. 즉시 로그아웃, 권한 회수, 토큰 탈취 대응이 필요하면 짧은 만료 시간, refresh token, 차단 목록, token version 같은 상태 관리가 다시 등장합니다.
| 비교 항목 | 세션 기반 | JWT 기반 |
|---|---|---|
| 클라이언트 값 | 보통 세션 ID | 서명된 claim 묶음 |
| 서버 상태 | 세션 저장소 필요 | access token 검증은 무상태 가능 |
| 강제 로그아웃 | 세션 삭제로 즉시 반영 가능 | 만료 전 무효화에는 별도 장치 필요 |
| 권한 변경 | 저장소의 세션/사용자 상태를 갱신 | 기존 토큰이 낡은 권한을 들 수 있음 |
| 네트워크 크기 | 작음 | claim과 서명 때문에 더 큼 |
| 보안 초점 | 세션 ID 난수성, 저장소 보호, 쿠키 속성 | 서명 알고리즘, 키 관리, iss/aud/exp 검증 |
서드파티 쿠키와 프라이버시
서드파티 쿠키는 현재 페이지와 다른 사이트가 설정하거나 받는 쿠키입니다. 예를 들어 쇼핑몰과 뉴스 사이트가 같은 광고 도메인의 픽셀을 포함하면, 그 광고 도메인은 여러 사이트 방문을 연결해 추적할 수 있습니다.
하지만 오늘날에는 “모든 브라우저가 서드파티 쿠키를 항상 자동 전송한다”고 설명하면 부정확합니다. 브라우저와 사용자 설정에 따라 서드파티 쿠키가 차단되거나, 파티션된 쿠키처럼 최상위 사이트별로 분리될 수 있습니다. Chrome도 2025년 4월 기준으로 서드파티 쿠키 선택권을 유지하면서 추적 보호를 강화하는 방향으로 업데이트했습니다.
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; Secure; 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를 살펴보겠습니다.