안동민 개발노트

본문 시작

쿠키와 세션

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

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

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

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

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

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


쿠키의 동작 원리

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

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

쿠키 식별자로 서버 세션을 찾는 왕복

성공한 로그인에서 서버는 세션 상태를 저장하고 브라우저에는 sid 쿠키를 보냅니다. 이후 브라우저가 전송 조건에 맞는 sid를 보내면 서버가 저장소를 조회하고 요청 권한을 확인합니다.

쿠키 식별자로 서버 세션을 찾는 왕복성공한 로그인에서 서버는 세션 상태를 저장하고 브라우저에는 sid 쿠키를 보냅니다. 이후 브라우저가 전송 조건에 맞는 sid를 보내면 서버가 저장소를 조회하고 요청 권한을 확인합니다.로그인 요청세션 저장저장 완료Set-Cookie: sidCookie: sidsid 조회사용자 상태권한 확인 후 응답브라우저앱 서버세션 저장소쿠키 식별자로 서버 세션을 찾는 왕복성공한 로그인에서 서버는 세션 상태를 저장하고 브라우저에는 sid 쿠키를 보냅니다. 이후 브라우저가 전송 조건에 맞는 sid를 보내면 서버가 저장소를 조회하고 요청 권한을 확인합니다.로그인 요청세션 저장저장 완료Set-Cookie:sidCookie: sidsid 조회사용자 상태권한 확인 후응답브라우저앱 서버세션저장소

정상 로그인과 유효한 세션의 예시입니다. Cookie에는 세션 식별자를 싣고 사용자 상태는 저장소에 둡니다. 실제 전송 조건·만료·권한 검사는 별도로 만족해야 하며, 아래 방문 횟수 데모를 실행한 흐름은 아닙니다.

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

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


쿠키 속성

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

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

  • Domain — 값 예시: example.com; 의미: 쿠키를 보낼 호스트 범위; 주의할 점: 생략하면 host-only 쿠키

  • Path — 값 예시: /api; 의미: 쿠키를 보낼 URL 경로 범위; 주의할 점: 보안 경계라기보다 전송 범위 필터

  • Expires — 값 예시: HTTP date; 의미: 절대 만료 시각; 주의할 점: 둘 다 있으면 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 토큰 기반 인증

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

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

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

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

세션 저장소와 서명 토큰의 상태 관리

세션 저장소와 서명 토큰의 상태 관리의 비교 기준과 해석 범위입니다.

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

두 방식 모두 서버의 인증·권한 검증이 필요합니다. JWT도 쿠키로 운반할 수 있으므로 저장 방식과 토큰 형식은 별개의 선택입니다.


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

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

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

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

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

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()

이 코드는 방문 횟수를 쿠키로 왕복시키는 데모로, 서버 세션이나 인증 예제가 아닙니다. visits를 정수로 바꾸는 입력 검증과 쿠키 헤더의 HTML 이스케이프가 빠져 있으며, 주석의 HTTPServer는 TLS를 설정하지 않습니다. Secure 쿠키는 보안 연결 조건과 브라우저의 localhost 예외 지원을 확인해야 합니다.

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