ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 로그인 쿠키가 있는데 다른 사이트 요청에는 안 붙는 이유
    Programming 2026. 9. 20. 10:53
    728x90
    반응형

    서로 다른 두 작은 문 모형 앞의 열쇠와 천 주머니
    인증 정보가 전달될 대상을 구분하는 AI 생성 개념 이미지입니다.

    쿠키 로그인 문제는 저장 여부, 전송 대상, SameSite 조건, 요청의 credentials, 응답 공유 순서로 확인하세요.

    개발자 도구에는 로그인 쿠키가 있는데 API는 비로그인 상태라고 합니다. 이때 SameSite를 None으로 바꾸는 것부터 시작하면 문제가 풀리지 않거나, 필요 이상으로 쿠키가 전송되는 범위를 넓힐 수 있습니다. 브라우저에 저장된 쿠키가 모든 요청에 붙는 것은 아니기 때문입니다.

    쿠키가 저장됐는지, 이번 요청의 목적지에 보낼 수 있는지, 실제로 보냈는지, 서버의 응답을 화면 코드가 읽을 수 있는지는 서로 다른 단계입니다. 이 글은 2026년 9월 13일 공식 문서 기준으로 그 단계를 나눕니다. 주소와 토큰은 설명용이며 실제 로그인이나 설정 변경은 하지 않았습니다.

    같은 사이트와 같은 출처는 다릅니다

    먼저 페이지 주소와 API 주소를 함께 적어 봅니다. HTTPS로 열린 app.example.test에서 HTTPS의 api.example.test API를 호출한다고 하겠습니다. 페이지가 다른 사이트의 iframe 안이 아니라 최상위 페이지로 열려 있다는 조건입니다.

    두 주소는 호스트가 다르므로 서로 다른 출처, 즉 cross-origin입니다. 출처는 프로토콜·호스트·포트를 함께 봅니다. 하지만 같은 HTTPS이고 등록 가능한 도메인이 같으므로 이 예시에서는 같은 사이트, 즉 same-site입니다. 사이트 판정에는 공개 접미사 규칙도 관여하므로 도메인의 마지막 두 조각만 무조건 비교하는 방식은 정확하지 않습니다. 사이트와 출처의 차이

    이 차이가 중요한 이유는 SameSite와 fetch의 credentials가 같은 질문을 하지 않기 때문입니다. SameSite는 사이트 간 요청이라는 맥락을 보고, credentials의 기본값인 same-origin은 출처를 기준으로 자격 증명 사용을 제한합니다.

    따라서 “서브도메인끼리니 같은 사이트다”라는 판단만으로 기본 fetch에 로그인 쿠키가 붙는다고 결론 내릴 수 없습니다. 반대로 서로 다른 출처라는 이유만으로 반드시 SameSite=None이 필요하다고 판단하는 것도 잘못입니다.

    저장 단계에서는 Set-Cookie와 실제 저장 결과를 봅니다

    가상의 API가 로그인 응답에 다음 헤더를 보낸다고 하겠습니다. 실제 비밀값이 아니라 속성을 설명하기 위한 예시입니다.

    Set-Cookie: session=example-token; Path=/; HttpOnly; Secure; SameSite=Lax
    

    Domain을 생략한 쿠키는 응답을 설정한 호스트에만 속하는 host-only 쿠키입니다. 이 예시에서는 api.example.test로 보내는 쿠키입니다. 페이지가 app.example.test에 있다는 이유만으로 Domain을 상위 도메인까지 넓힐 필요는 없습니다. 요청의 목적지가 API 호스트인지가 먼저입니다. Domain과 host-only 쿠키

    Path=/는 이 호스트의 경로 전반을 대상으로 합니다. Secure는 보안 연결 조건이고, HttpOnly는 JavaScript에서 쿠키 값을 직접 읽지 못하게 하는 속성입니다. HttpOnly가 있다고 fetch 요청에 쿠키를 보내지 않는 것은 아닙니다.

    이 때문에 document.cookie에서 값이 안 보인다는 사실만으로 저장 실패를 판정하면 안 됩니다. 응답의 Set-Cookie와 브라우저 저장소 화면, 쿠키가 거부됐다면 그 이유를 함께 확인합니다. 프런트엔드 코드에서 response.headers.get("Set-Cookie")로 읽으려는 것도 적절한 확인 방법이 아닙니다. 브라우저는 이 응답 헤더를 스크립트에 노출하지 않습니다. Set-Cookie의 JavaScript 접근 제한

    로그인 요청 자체가 cross-origin fetch라면 credentials 설정은 응답의 쿠키를 받아들이는 데에도 영향을 줍니다. 이후의 /me 요청만 고치고 로그인 응답 단계는 놓치지 않아야 합니다. 서버가 헤더를 보냈다는 기록과 브라우저가 저장했다는 기록은 별도로 남깁니다.

    이번 요청에 쿠키를 보낼 수 있는지 확인합니다

    이제 app.example.test의 코드가 API의 사용자 정보를 읽는다고 하겠습니다.

    const response = await fetch("https://api.example.test/me", {
      credentials: "include",
    });
    
    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }
    const me = await response.json();
    

    credentials의 기본값은 same-origin입니다. 이 예시처럼 다른 출처로 보내는 요청에서 쿠키를 사용하려면 include를 검토합니다. 이 옵션은 요청에 자격 증명을 포함하는 것과 응답의 Set-Cookie를 존중할지를 제어합니다. Request.credentials

    다만 include는 모든 쿠키 제한을 무시하라는 뜻이 아닙니다. 쿠키의 목적지 호스트와 경로, 만료 여부, Secure, SameSite, 브라우저의 쿠키 정책을 여전히 만족해야 합니다. API에 저장된 쿠키가 다른 API 호스트로 리다이렉트된 요청에도 그대로 붙는다고 가정해서는 안 됩니다.

    이 예시의 조건을 한 줄씩 대조해 보면 이해하기 쉽습니다. 쿠키는 API 호스트 소유이고, 요청 목적지도 같은 API 호스트입니다. 경로 /me는 Path=/ 범위 안에 있으며 HTTPS를 사용합니다. 페이지와 API는 앞서 정한 조건에서 same-site이고, fetch에는 include가 있습니다.

    이렇게 조건이 맞아야 “전송될 수 있다”고 판단할 수 있습니다. 실제 전송 여부는 Network에서 해당 요청의 Cookie 헤더와 브라우저의 차단 사유를 확인합니다. 쿠키 저장소에 보인다는 이유로 전송 결과까지 추정하지 않습니다.

    SameSite=Lax가 막는 요청은 따로 있습니다

    이번에는 페이지가 https://other.test에 있다고 바꿔 보겠습니다. 이 페이지에서 api.example.test로 fetch를 보내면 앞의 서브도메인 예시와 달리 cross-site 요청입니다. 같은 코드에 include를 넣더라도 SameSite=Lax 쿠키를 이 cross-site fetch에 보내도록 강제하지 못합니다.

    Lax에는 교차 사이트의 최상위 탐색 중 안전한 메서드를 쓰는 경우와 같은 예외가 있습니다. 하지만 새 페이지로 이동하는 링크와 백그라운드 fetch는 같은 요청 형태가 아닙니다. 링크로 API 주소를 열었을 때 동작한다고 fetch도 같아야 한다고 비교하면 조건을 놓칩니다. SameSite의 요청 조건

    SameSite=None은 교차 사이트 전송이 필요한 설계에서 검토할 수 있으며 Secure가 함께 필요합니다. 그러나 이 두 속성을 붙였다고 모든 브라우저의 서드파티 쿠키 제한까지 해제되는 것은 아닙니다. 브라우저 설정, 사생활 보호 모드, 최상위 사이트와 임베드 관계 등에 따라 차단이나 분할 저장 정책이 적용될 수 있습니다. 서드파티 쿠키 정책

    따라서 None으로 바꾸기 전에 실제 서비스가 서로 다른 사이트에서 쿠키 인증을 해야 하는지부터 확인합니다. 같은 사이트의 앱과 API 통신이라면 credentials나 목적지 범위가 원인일 수 있습니다. 다른 사이트에 삽입되는 서비스라면 쿠키 속성뿐 아니라 해당 브라우저에서 지원하는 인증 흐름까지 함께 검토해야 합니다.

    쿠키 전송과 CORS 응답 공유도 별개입니다

    쿠키를 보내는 cross-origin 요청의 응답을 JavaScript에서 읽으려면 서버의 CORS 설정도 맞아야 합니다. 앞의 app.example.test 예시에서 핵심 응답 헤더는 다음과 같습니다. 서버 실측값이 아니라 허용 정책의 예시입니다.

    Access-Control-Allow-Origin: https://app.example.test
    Access-Control-Allow-Credentials: true
    Vary: Origin
    

    자격 증명을 쓰는 응답에 Allow-Origin의 별표를 넣으면 같은 방식으로 읽기를 허용할 수 없습니다. 허용할 origin을 정확히 지정하고, 여러 origin을 지원할 때는 서버에서 허용 목록과 대조해야 합니다. credentials를 포함한 fetch와 CORS

    이때 CORS 오류가 곧 쿠키 전송 실패라는 뜻은 아닙니다. 사전 요청이 필요 없는 요청은 서버에 쿠키와 함께 도착했지만 브라우저가 응답 공유를 막을 수 있습니다. 응답을 못 읽는 문제를 로그인 쿠키가 없어진 문제로 오해하지 않아야 합니다.

    반대로 사용자 지정 헤더나 메서드 때문에 preflight가 필요하다면 사전 요청이 먼저 통과해야 합니다. 일반적인 CORS 사전 요청에는 로그인 쿠키가 포함되지 않습니다. OPTIONS에도 로그인 세션을 요구해 차단하면 본 요청의 인증을 확인하기 전에 흐름이 멈출 수 있습니다. 자격 증명과 사전 요청

    서버가 401을 반환한 경우도 구분합니다. Cookie 헤더가 전송됐는데 401이라면 세션 만료, 서버의 세션 해석, 요청 대상 등의 문제를 확인할 차례입니다. 브라우저가 쿠키를 보내지 않은 경우와 같은 처방을 적용하지 않습니다.

    오류가 난 단계를 적으면 수정 범위가 줄어듭니다

    첫 번째 경우는 로그인 응답에 Set-Cookie는 있지만 저장 결과가 없는 상황입니다. 로그인 요청의 credentials, 브라우저의 거부 사유, 속성의 유효성을 확인합니다. 만료된 쿠키를 설정하거나 지우는 응답인지도 살펴봅니다.

    두 번째는 저장소에는 쿠키가 있지만 /me 요청에 Cookie가 없는 상황입니다. 목적지 호스트·경로, HTTPS, SameSite와 최상위 사이트 맥락, credentials를 차례로 대조합니다. 같은 이름의 쿠키가 서로 다른 경로나 도메인에 있으면 이름만 보고 동일한 쿠키라고 판단하지 않습니다.

    세 번째는 요청에 Cookie가 있지만 화면은 비로그인으로 보이는 상황입니다. 응답 상태와 CORS 헤더, 프런트엔드의 오류 처리를 확인합니다. 응답 공유가 실패했는지, 서버가 세션을 거부했는지에 따라 수정할 위치가 달라집니다.

    진단 자료에는 실제 토큰 대신 쿠키 이름과 속성, 비밀값을 지운 요청·응답 헤더를 남깁니다. 모든 사이트의 쿠키를 한꺼번에 지우기 전에 문제가 난 호스트와 요청을 먼저 좁히는 편이 비교에도 유리합니다.

    해결의 기준은 쿠키가 “있다”는 화면이 아닙니다. 의도한 요청에만 쿠키가 전송되고, 서버가 세션을 인정하며, 허용한 페이지에서 그 응답을 읽을 수 있어야 합니다. 이 세 결과를 따로 확인하면 SameSite를 무작정 완화하지 않고도 로그인 문제를 설명하고 고칠 수 있습니다.

    728x90
    반응형
Designed by Tistory.