it-box

쿠키 속성 정리와 로그인이 풀리는 원인

Set-Cookie 속성의 의미와 조합, 서브도메인 공유 방식, 로그인이 풀리는 흔한 원인, 요청 헤더와 응답 헤더에서 쿠키를 읽는 방법을 정리합니다.

로그인이 몇 분 뒤에 풀리는 상황

로그인 기능을 붙인 뒤 "로그인은 되는데 새로고침하면 로그아웃된다"는 문제가 자주 보고됩니다. 원인은 서버 코드보다 쿠키 속성에 있는 경우가 많습니다. 쿠키가 저장되지 않았거나, 저장되었어도 다음 요청에 실리지 않으면 서버는 세션을 찾지 못합니다.

쿠키 문제를 풀려면 세 가지를 확인해야 합니다. 서버가 어떤 Set-Cookie를 보냈는지, 브라우저가 그 쿠키를 저장했는지, 그다음 요청의 Cookie 헤더에 실렸는지입니다. 이 글은 각 단계에서 무엇을 봐야 하는지 속성 단위로 정리합니다.

Set-Cookie의 구조

서버는 응답 헤더에 쿠키를 설정합니다. 첫 부분이 이름과 값이고, 뒤에 세미콜론으로 구분된 속성이 붙습니다.

Set-Cookie: sid=8f2c91; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax

브라우저는 이 값을 읽어 쿠키를 저장하고, 조건이 맞는 요청에만 Cookie 헤더로 되돌려 보냅니다. 속성은 쿠키를 저장할지, 어떤 요청에 실을지, 스크립트가 읽을 수 있을지를 결정합니다.

속성별 의미

HttpOnly는 자바스크립트의 document.cookie로 쿠키를 읽지 못하게 합니다. XSS 취약점이 생겼을 때 세션 값이 탈취되는 것을 막는 가장 기본적인 방어입니다. 다만 쿠키가 요청에 자동으로 실리는 것은 막지 못하므로, CSRF 방어는 따로 필요합니다.

Secure는 HTTPS 연결에서만 쿠키를 보내도록 합니다. 개발 중에 http://localhost에서 Secure 쿠키를 설정하면 브라우저에 따라 저장되지 않을 수 있습니다. 이때는 로컬 개발 환경에서만 HTTPS를 쓰거나 설정을 분리해야 합니다.

SameSite는 다른 사이트에서 시작된 요청에 쿠키를 얼마나 실을지 정합니다.

  • Strict: 다른 사이트에서 시작된 요청에는 쿠키를 보내지 않습니다. 외부 링크로 들어온 첫 요청에서도 로그인 상태가 보이지 않을 수 있습니다.
  • Lax: 최상위 페이지 이동처럼 안전한 메서드(주로 GET)로 들어오는 요청에는 보내고, 폼 POST나 iframe, 이미지 같은 요청에는 보내지 않습니다. 대부분의 웹 서비스가 기본으로 쓰기 좋은 값입니다.
  • None: 다른 사이트 요청에도 항상 보냅니다. 반드시 Secure와 함께 써야 하며, 같이 쓰지 않으면 브라우저가 쿠키를 거부합니다.

Domain을 생략하면 쿠키는 그 응답을 준 정확한 호스트에만 저장됩니다. 이를 호스트 전용(host-only) 쿠키라고 합니다. Domain=example.com을 명시하면 example.com과 모든 서브도메인이 쿠키를 받을 수 있습니다. 설정 방식에 따라 api.example.com과 www.example.com 사이에 세션을 공유할 수 있는 이유가 여기에 있습니다.

Path는 쿠키를 보낼 URL 경로의 범위입니다. Path=/로 두면 사이트 전체에서 쓰고, Path=/admin으로 두면 /admin으로 시작하는 요청에만 실립니다. 같은 이름의 쿠키가 경로별로 여러 개 생길 수 있습니다.

Max-Age와 Expires는 쿠키의 수명을 정합니다. Max-Age는 초 단위의 상대 시간이고, Expires는 날짜로 지정하는 절대 시간입니다. 두 속성이 같이 있으면 Max-Age가 우선합니다. 생략하면 세션 쿠키가 되어 브라우저를 닫을 때 사라집니다.

조합으로 보는 실무 설정

쿠키의 용도에 따라 속성 조합이 달라집니다. 로그인 세션 쿠키는 다음처럼 설정하는 것이 일반적입니다.

Set-Cookie: sid=8f2c91; Path=/; HttpOnly; Secure; SameSite=Lax

여기에 Max-Age를 붙이면 세션 수명을 정할 수 있습니다. 화면 테마처럼 스크립트가 읽어야 하는 값은 HttpOnly를 붙이지 않습니다. 다른 사이트의 iframe에 들어가는 위젯이 쿠키를 사용해야 한다면 SameSite=None; Secure가 필요하지만, 이때는 사용자 추적에 쓰일 수 있으므로 개인정보 처리 방침과 함께 검토해야 합니다.

__Host- 접두사를 붙이면 브라우저가 Secure, Path=/, Domain 없음 조건을 강제합니다. 예를 들어 __Host-sid라는 이름은 서브도메인이 같은 이름의 쿠키를 덮어쓰는 공격을 줄이는 데 도움이 됩니다. 이 규칙은 쿠키 이름과 속성을 함께 검사하므로, 이름만 바꾸고 Domain을 넣으면 쿠키가 거부됩니다.

로그인이 풀리는 흔한 원인

  • 개발 환경에서 Secure 쿠키를 http://로 설정합니다. 브라우저에 따라 쿠키가 저장되지 않아서 로그인이 계속 되지 않은 상태로 보입니다.
  • SameSite=Strict나 Lax 때문에 다른 사이트에서 돌아온 콜백 요청에 쿠키가 빠집니다. 결제나 소셜 로그인 후 리다이렉트에서 자주 나타납니다.
  • Domain이나 Path가 맞지 않아 설정한 쿠키가 다음 요청 대상에 포함되지 않습니다. 예를 들어 Path=/app으로 설정하고 /api를 호출하면 쿠키가 실리지 않습니다.
  • Max-Age나 Expires가 너무 짧거나, 서버 시각과 클라이언트 시각이 크게 다릅니다.
  • 같은 이름의 쿠키가 Path나 Domain만 다르게 여러 개 존재합니다. 서버가 어느 쿠키를 받는지에 따라 세션이 번갈아 바뀔 수 있습니다.
  • 브라우저가 서드파티 쿠키를 차단하는 설정에서 다른 도메인의 인증 쿠키를 쓰는 구조입니다.

이런 경우에는 서버 로그를 보기 전에 응답의 Set-Cookie와 다음 요청의 Cookie를 나란히 비교하는 것이 가장 빠릅니다.

요청 헤더에서 쿠키 읽기

브라우저가 보내는 요청의 Cookie 헤더는 이름과 값의 목록만 담고 있어서 속성은 없습니다. Cookie 파서에 다음 값을 넣으면 쌍이 나뉘어 보입니다.

Cookie: session=abc==; theme=dark; session=xyz; empty=
[
  {"name":"session","value":"abc=="},
  {"name":"theme","value":"dark"},
  {"name":"session","value":"xyz"},
  {"name":"empty","value":""}
]

이 예제에는 중요한 점이 두 가지 있습니다. 첫째, 값에 =가 들어 있어도 첫 번째 =를 기준으로 나뉩니다. abc==가 그대로 값이 됩니다. 둘째, 같은 이름의 session이 두 번 나옵니다. 프레임워크에 따라 첫 번째 값이나 마지막 값을 고르므로, 중복 쿠키가 있으면 세션이 예상과 다르게 선택될 수 있습니다. 이 파서는 중복을 합치지 않고 모두 보여 줍니다.

형식이 깨진 값도 확인할 수 있습니다. a=1;b처럼 등호가 없는 조각이 있으면 파서는 형식 오류를 표시합니다. 이 경우 쿠키 이름이 누락되었거나 세미콜론이 잘못 들어간 것이므로 헤더를 만든 코드를 확인해야 합니다.

응답 헤더에서 Set-Cookie 읽기

서버가 보낸 응답 헤더를 확인하려면 HTTP 헤더 파서를 씁니다. 이 파서는 시작 줄과 헤더를 나누고, 헤더 이름은 소문자로 바꾸고, 같은 이름의 헤더는 값 배열로 모읍니다. Set-Cookie는 여러 개를 보낼 수 있으므로 배열로 보입니다.

HTTP/1.1 200 OK
Set-Cookie: a=1; HttpOnly
Set-Cookie: b=2; Secure
Content-Type: text/html

파서 결과는 다음과 같습니다.

{
  "startLine": "HTTP/1.1 200 OK",
  "headers": {
    "set-cookie": ["a=1; HttpOnly", "b=2; Secure"],
    "content-type": ["text/html"]
  }
}

이 도구는 Set-Cookie의 속성을 따로 해석하지 않고 원래 문자열을 그대로 보여 줍니다. 속성이 맞는지는 위의 속성 설명과 대조해서 직접 읽어야 합니다. 위 예에서 b=2는 Secure가 있으므로 HTTP 연결에서는 저장되지 않습니다. 두 쿠키의 속성을 같은 방식으로 비교해야 하는 이유입니다.

점검 목록

  • 응답에 Set-Cookie가 실제로 있는지, 여러 줄로 모두 나오는지 확인합니다.
  • 세션 쿠키에 HttpOnly와 Secure가 있는지 확인합니다.
  • SameSite 값이 로그인 콜백 흐름과 맞는지 확인합니다. None을 쓰면 Secure가 함께 있어야 합니다.
  • Domain을 명시했다면 서브도메인 공유가 의도한 것인지 확인합니다.
  • Path가 실제 API 경로를 포함하는지 확인합니다.
  • Max-Age와 Expires가 의도한 수명과 맞는지 확인합니다.
  • 같은 이름의 쿠키가 중복되어 있지 않은지 요청 헤더에서 확인합니다.
  • 개발 환경과 운영 환경에서 Secure 설정이 다르게 동작하지 않는지 확인합니다.

관련 도구