본문으로 건너뛰기

안동민 개발노트

본문 시작

쿠키와 세션

쿠키의 저장·전송 조건과 SameSite 보안을 익히고 서버 세션·토큰 인증 및 서드파티 쿠키의 차이를 비교합니다.

HTTP는 무상태(Stateless) 프로토콜입니다.

각 요청은 그 자체로 해석될 수 있어야 하고, 같은 TCP 연결을 탔는지 여부가 요청의 의미를 결정하지 않습니다.

이 성질 덕분에 HTTP는 단순하고 확장하기 쉽지만, 로그인 상태나 장바구니처럼 “이전 요청의 결과”가 필요한 웹 애플리케이션은 별도의 상태 관리 장치가 필요합니다.

HTTP는 무상태라서 로그인 상태를 별도로 운반해야 한다

요청 하나만 보면 사용자의 이전 행동을 알 수 없다. 쿠키와 세션은 이 간격을 메우는 상태 전달 장치다.

  1. 1. 독립 요청

    독립 요청 HTTP 요청은 같은 연결을 탔는지와 무관하게 해석되어야 한다.

  2. 2. 상태 필요

    상태 필요 로그인, 장바구니, 선호 설정은 이전 요청의 결과를 필요로 한다.

  3. 3. 쿠키/세션

    쿠키/세션 브라우저는 작은 식별자를 보내고 서버가 사용자 상태를 찾는다.

쿠키는 이 간격을 메우기 위해 서버와 브라우저가 합의한 작은 상태 전달 메커니즘입니다.

서버는 Set-Cookie 응답 헤더로 이름, 값, 범위, 만료, 보안 속성을 보내고, 브라우저는 이후 요청에서 조건에 맞는 쿠키만 Cookie 요청 헤더에 실어 보냅니다.


쿠키의 동작 원리

쿠키 자체가 사용자를 “증명”하는 것은 아닙니다.

보통 브라우저는 예측하기 어려운 세션 식별자를 쿠키에 저장하고, 서버는 그 식별자를 세션 저장소의 사용자 상태와 매핑합니다.

즉, 쿠키는 상태 데이터 전체가 아니라 서버가 상태를 찾기 위한 열쇠인 경우가 많습니다.

서버가 Set-Cookie로 저장시키고, 브라우저가 조건에 맞을 때만 보낸다

쿠키 값이 있다는 사실만으로 충분하지 않다. 현재 요청이 Domain, Path, Secure, SameSite, 만료 조건을 만족해야 전송된다.

  1. 응답: Set-Cookie

    서버가 이름, 값, 범위, 만료, 보안 속성을 보낸다.

  2. 저장: 브라우저

    브라우저가 속성 조건과 함께 쿠키 저장소에 기록한다.

  3. 요청: Cookie

    다음 요청이 조건을 만족할 때만 Cookie 헤더에 포함한다.

조건판단 내용
Domain / Path현재 호스트와 URL 경로가 쿠키 범위 안인가?
SecureHTTPS 요청인가?
SameSite크로스사이트 요청에서 전송 가능한가?
Expires / Max-Age아직 만료되지 않았는가?

브라우저는 쿠키를 보낼 때 Domain, Path, Secure, SameSite, 만료 시각 같은 조건을 함께 평가합니다.

그래서 쿠키 문제를 디버깅할 때는 “쿠키 값이 있는가”뿐 아니라 “현재 요청이 쿠키의 전송 조건을 만족하는가”를 같이 봐야 합니다.


쿠키 속성

쿠키 속성은 저장 위치, 전송 범위, 수명, 스크립트 접근 여부, 크로스사이트 전송 여부를 제어합니다.

특히 인증 쿠키는 값 자체보다 스코프와 전송 조건을 좁히는 것이 중요합니다.

쿠키 속성은 값보다 전송 범위와 보안 조건을 좁히는 장치다

인증 쿠키는 특히 스코프, 수명, 스크립트 접근, 크로스사이트 전송 조건을 명확히 제한해야 한다.

  1. 전송 범위

    Domain / Path 전송 범위 어느 호스트와 경로에 쿠키를 보낼지 결정한다.

  2. 수명

    Expires / Max-Age 수명 언제까지 저장하고, 언제 삭제할지 결정한다.

  3. 보안 조건

    HttpOnly / Secure 보안 조건 스크립트 접근과 평문 전송 위험을 줄인다.

  4. 크로스사이트 전송

    SameSite 크로스사이트 전송 Lax, Strict, None으로 외부 사이트 요청 동반 여부를 제한한다.

  5. 식별자

    Name / Value 식별자 대개 사용자 정보 전체가 아니라 세션을 찾는 열쇠를 담는다.

  6. 스코프 강제

    Prefix 스코프 강제 __Host- 같은 접두사는 안전한 속성 조합을 요구한다.

속성값 예시의미주의할 점
Domainexample.com쿠키를 보낼 호스트 범위생략하면 현재 호스트 전용에 가까운 동작
Path/api쿠키를 보낼 URL 경로 범위보안 경계라기보다 전송 범위 필터
ExpiresHTTP date절대 만료 시각Max-Age가 있으면 보통 Max-Age가 우선
Max-Age3600상대 만료 시간(초)음수나 0이면 삭제 용도로 쓰임
HttpOnly플래그JS의 document.cookie 접근 제한XSS 자체를 막지는 않지만 세션 탈취 피해를 줄임
Secure플래그보안 연결에서만 전송HTTPS 배포의 기본값으로 두는 편이 안전
SameSiteLax/Strict/None크로스사이트 요청 전송 제어CSRF 완전 대체가 아니라 방어층 중 하나

SameSite와 CSRF

CSRF는 공격자가 사용자의 브라우저로 피해 사이트에 요청을 보내게 만드는 공격입니다.

쿠키 기반 로그인은 브라우저가 쿠키를 자동으로 붙여 보내기 때문에, 서버가 요청의 의도와 출처를 별도로 확인하지 않으면 문제가 됩니다.

SameSite는 쿠키 자동 전송을 줄이지만 CSRF 방어를 완성하지는 않는다

브라우저가 쿠키를 자동으로 붙여 보내는 성질 때문에, 서버는 요청의 의도와 출처를 별도로 확인해야 한다.

  1. 공격 페이지

    사용자 브라우저로 피해 사이트에 요청을 만들 수 있다.

  2. 브라우저 판단

    SameSite 조건에 따라 쿠키 전송 여부를 결정한다.

  3. 서버 검증

    Origin, Referer, CSRF token, 메서드 안전성을 확인한다.

  4. 상태 변경

    검증된 요청만 장바구니, 송금, 설정 변경을 수행한다.

  5. SameSite=Lax

    많은 크로스사이트 POST를 줄이지만 모든 우회를 막지는 않는다.

  6. CSRF token

    사용자 의도를 서버가 확인할 수 있는 별도 값을 둔다.

  7. Origin 검증

    중요한 상태 변경 요청은 출처를 함께 확인한다.

SameSite=Lax는 많은 크로스사이트 POST 경로를 줄여주지만, 안전하지 않은 GET 엔드포인트, 같은 registrable domain의 하위 도메인, 오래된 클라이언트, 클라이언트 측 CSRF 같은 경우까지 해결하지는 않습니다.

중요한 상태 변경 요청은 CSRF 토큰, Origin/Referer 검증, 안전한 메서드 설계를 함께 사용해야 합니다.


안전한 쿠키 설정 예시

인증 세션 쿠키는 가능한 한 값은 무작위 식별자로 두고, 브라우저가 그 값을 어디에 저장하고 언제 보내는지를 좁혀야 합니다.

아래 설정은 일반적인 웹 애플리케이션의 기본 출발점입니다.

인증 쿠키는 무작위 식별자와 좁은 전송 조건으로 설계한다

쿠키 속성은 권한 검사를 대신하지 않는다. 서버 검증, 만료, 로그아웃 처리가 함께 있어야 안전하다.

  1. __Host-

    Domain 미지정, Path=/, Secure 요구로 호스트 범위를 좁힌다.

  2. HttpOnly

    document.cookie 접근을 막아 XSS 피해를 줄인다.

  3. Secure

    HTTPS 요청에서만 쿠키를 전송한다.

  4. SameSite=Lax

    기본적인 크로스사이트 자동 전송을 줄인다.

Set-Cookie: __Host-session=K8x...; Path=/; Max-Age=86400; Secure; HttpOnly; SameSite=Lax

__Host- 접두사는 Secure, Path=/, Domain 미지정을 요구하는 방향으로 동작해 쿠키 스코프를 호스트 단위로 좁히는 데 도움이 됩니다.

단, 모든 보안은 서버 검증과 함께 완성됩니다.

쿠키 속성만으로 권한 검사, 세션 만료, 재발급, 로그아웃 처리를 대신할 수는 없습니다.


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

세션 기반 인증은 서버가 상태를 보관하고, 브라우저는 세션 ID만 들고 다닙니다.

이 구조는 강제 로그아웃, 권한 변경 반영, 세션 만료 관리가 비교적 직접적입니다.

대신 여러 서버가 있는 환경에서는 세션 저장소를 공유하거나 sticky session, 중앙 저장소 같은 설계가 필요합니다.

세션은 서버 저장소를 보고, JWT는 서명된 claim을 검증한다

JWT가 무상태처럼 보이더라도 로그아웃, 권한 회수, 탈취 대응을 하려면 다시 운영 상태가 필요해진다.

  1. 세션 기반

    클라이언트 값 작은 세션 ID 서버 상태 세션 저장소가 사용자 상태를 보관 강제 로그아웃 세션 삭제로 즉시 반영 가능

  2. JWT 기반

    클라이언트 값 claim과 서명을 포함한 토큰 서버 상태 access token 검증은 저장소 없이 가능 운영 부담 만료 전 무효화에는 차단 목록이나 token version 필요

토큰 기반 인증, 특히 JWT는 토큰 안에 claim을 담고 서명으로 무결성을 검증합니다.

이 덕분에 리소스 서버가 중앙 세션 저장소를 매번 조회하지 않고도 토큰을 검증할 수 있습니다.

하지만 토큰이 만료되기 전까지 살아 있다는 점은 운영상 부담이 됩니다.

즉시 로그아웃, 권한 회수, 토큰 탈취 대응이 필요하면 짧은 만료 시간, refresh token, 차단 목록, token version 같은 상태 관리가 다시 등장합니다.

비교 항목세션 기반JWT 기반
클라이언트 값보통 세션 ID서명된 claim 묶음
서버 상태세션 저장소 필요access token 검증은 무상태 가능
강제 로그아웃세션 삭제로 즉시 반영 가능만료 전 무효화에는 별도 장치 필요
권한 변경저장소의 세션/사용자 상태를 갱신기존 토큰이 낡은 권한을 들 수 있음
네트워크 크기작음claim과 서명 때문에 더 큼
보안 초점세션 ID 난수성, 저장소 보호, 쿠키 속성서명 알고리즘, 키 관리, iss/aud/exp 검증

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

서드파티 쿠키는 현재 페이지와 다른 사이트가 설정하거나 받는 쿠키입니다.

예를 들어 쇼핑몰과 뉴스 사이트가 같은 광고 도메인의 픽셀을 포함하면, 그 광고 도메인은 여러 사이트 방문을 연결해 추적할 수 있습니다.

서드파티 쿠키 추적은 브라우저 정책과 사용자 설정의 영향을 받는다

같은 제3자 리소스가 여러 사이트에 포함되면 방문을 연결할 수 있지만, 현대 브라우저는 차단과 파티션 정책을 강화하고 있다.

  1. news.example

    광고 픽셀을 포함한 페이지

  2. ads.example

    여러 사이트에서 같은 제3자 쿠키를 받을 수 있음

  3. 추적 위험

    사이트 A와 B 방문을 같은 제3자 식별자로 연결할 수 있다.

  4. 브라우저 차단

    서드파티 쿠키 전송이 기본 차단되거나 제한될 수 있다.

  5. 파티션

    최상위 사이트별로 쿠키 저장 공간을 분리할 수 있다.

하지만 오늘날에는 “모든 브라우저가 서드파티 쿠키를 항상 자동 전송한다”고 설명하면 부정확합니다.

브라우저와 사용자 설정에 따라 서드파티 쿠키가 차단되거나, 파티션된 쿠키처럼 최상위 사이트별로 분리될 수 있습니다.

Chrome도 2025년 4월 기준으로 서드파티 쿠키 선택권을 유지하면서 추적 보호를 강화하는 방향으로 업데이트했습니다.

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; 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를 살펴보겠습니다.