HTTP 헤더 (캐싱, CORS)

문린이·2023년 3월 29일
0

같은 API를 바라보는 두 개의 클라이언트가 있을 때 (A 클라이언트, B 클라이언트) A 클라이언트에서 뒤로 가기를 눌러서 B 클라이언트로 갈 때 CORS 에러가 나온 에러를 해결한 회고록입니다.

캐싱이랑 관련된 옵션

헤더에 적절한 값을 설정하면 캐싱 동작을 최적화하고 서버에 대한 요청 수를 줄일 수 있다.
그러나 잘못된 값으로 인해 사용자에게 오래된 콘텐츠가 제공되는 등의 문제가 발생할 수 있으므로 캐싱 헤더를 설정할 때 주의해야 한다.

  1. Cache-Control: 캐싱 동작을 제어하는데 사용할 수 있는 일반 헤더이다. 여기에는 "max-age", "no-cache", "no-store", "must-revalidate" 및 "public"과 같은 다양한 지시문이 포함된다.
    예를 들어 "max-age"는 리소스가 서버에서 재검증되기 전에 캐시될 수 있는 최대 시간(초)을 지정한다. "no-cache" 및 "no-store"는 모두 캐싱을 방지하는 반면 "must-revalidate"는 캐시된 응답을 다시 제공하기 전에 서버에서 다시 유효성을 검사해야 함을 나타낸다.

    • max-age: 이 지시문은 응답이 최신으로 간주될 수 있는 최대 기간(초)을 지정한다. 예를 들어 "Cache-Control: max-age=3600"은 서버에 새 요청을 하기 전에 3600초(1시간) 동안 응답을 캐시하도록 클라이언트에 지시한다.
    • no-cache: 이 지시문은 클라이언트에게 캐시된 사본을 사용하기 전에 서버와의 응답을 재확인하도록 지시한다. 캐시된 복사본은 아직 최신 상태이면 사용할 수 있지만 만료되었거나 서버가 다른 ETag를 반환하는 경우 클라이언트는 서버에 새 요청을 해야 한다.
    • no-store: 이 지시문은 헤더와 본문을 포함하여 응답의 어떤 부분도 저장하지 않도록 클라이언트에 지시한다. 이렇게 하면 응답이 중간 캐시에 의해 캐시되거나 디스크에 저장되지 않아 민감한 정보를 보호할 수 있습니다.
    • must-revalidate: 이 지시문은 캐시된 복사본을 사용하기 전에 클라이언트가 아직 신선하더라도 서버와 응답을 재검증하도록 지시한다. 서버를 사용할 수 없거나 응답이 만료된 경우 클라이언트는 캐시된 복사본을 사용하지 않아야 한다.
    • private: 이 지시문은 응답이 단일 사용자를 위한 것이며 다른 사용자에게 캐시되거나 제공되어서는 안 된다는 것을 중개자(예: 프록시)에게 알려줍니다. 이는 다른 사용자와 공유해서는 안 되는 중요한 정보가 포함된 응답에 유용하다.
    • public: 이 지시문은 응답이 캐시되어 모든 사용자에게 제공될 수 있음을 중개자에게 알려준다. Cache-Control 헤더가 없는 경우 이것이 기본 캐싱 동작이다.
  2. Expires: 이 헤더는 캐시된 응답이 오래된 것으로 간주되어 서버에서 다시 유효성을 검사해야 하는 날짜와 시간을 지정한다. 값은 GMT 형식의 타임스탬프이다.
    그러나 Expires는 Cache-Control로 대체되었으며 일부 브라우저에서는 이를 지원하지 않을 수 있다.

  3. ETag: 캐시된 응답이 여전히 유효한지 확인하는 데 사용할 수 있는 고유 식별자이다. 리소스가 요청되면 서버는 ETag를 생성하고 응답 헤더에 다시 보낸다. 리소스가 다시 요청되면 클라이언트는 "If-None-Match" 헤더에 ETag를 포함한다. ETag가 일치하면 서버는 전체 콘텐츠 대신 304 Not Modified 응답을 반환하여 캐시된 응답을 계속 사용할 수 있음을 나타낸다.

  4. Last-Modified: 이 헤더는 리소스가 서버에서 마지막으로 수정된 시간을 나타낸다. 리소스가 요청되면 서버는 응답으로 Last-Modified 헤더를 다시 보낸다. 리소스가 다시 요청되면 클라이언트는 "If-Modified-Since" 헤더에 Last-Modified 값을 포함한다. 값이 일치하면 서버는 캐시된 응답을 계속 사용할 수 있음을 나타내는 304 Not Modified 응답을 반환한다.

인코딩이랑 관련된 옵션

  1. Vary : 이 헤더는 캐시된 응답을 후속 요청에 사용할 수 있는지 여부를 결정하기 위해 캐시에서 사용하는 기준을 나타내는 HTTP 응답 헤더이다. 응답이 Vary 헤더 필드에 나열된 하나 이상의 요청 헤더 필드 값에 민감하다는 것을 캐시에 알린다.

    Vary: Accept-Encoding

    클라이언트가 서버에 요청을 보낼 때 Vary 헤더를 사용하여 "Accept-Encoding" 헤더와 같은 특정 헤더 필드의 값에 따라 응답이 다를 수 있음을 클라이언트에 알릴 수 있다. 이를 통해 클라이언트는 향후 요청에 해당 헤더 필드를 포함하여 올바른 응답을 받을 수 있다.
    예를 들어 서버가 Vary 헤더를 "Accept-Encoding"으로 설정하여 응답을 보내는 경우 클라이언트에서 지정한 인코딩에 따라 응답이 달라질 수 있음을 의미한다. 클라이언트가 다른 인코딩으로 후속 요청을 하는 경우 서버는 캐시된 응답을 사용하지 않고 대신 적절한 인코딩으로 새 응답을 생성해야 한다.
    Vary 헤더는 캐시가 동일한 헤더를 가진 향후 요청에 대한 응답을 재사용할 수 있도록 하여 캐싱을 최적화하고 네트워크 트래픽을 줄이는 데 유용하다. 그러나 예기치 않은 동작이나 캐시 중독을 방지하려면 신중하고 올바르게 사용하는 것이 중요합니다.
    Vary 헤더가 CDN 및 프록시가 캐싱을 처리하는 방식에 영향을 미칠 수 있다는 점도 주목할 가치가 있다.
    응답에 Vary 헤더가 포함된 경우 해당 엔터티가 Vary 헤더 필드에 나열된 헤더 값을 기반으로 응답의 여러 버전을 캐시할 수 없는 한 CDN 또는 프록시에 의해 캐시되지 않을 수 있다.

  2. Content-Encoding: 이 헤더는 HTTP 메시지 본문의 인코딩을 나타내는 데 사용되는 HTTP 헤더 필드이다. 서버와 클라이언트 간에 전송되는 데이터를 압축하는 데 사용된다. 콘텐츠 인코딩 필드는 데이터를 압축하는 데 사용된 인코딩 유형을 클라이언트에 알리는 데 사용된다.
    (Vary: Accept-Encoding 및 Content-Encoding: gzip 헤더를 함께 사용하는 것이 일반적이다.)

    가장 일반적으로 사용되는 콘텐츠 인코딩은 gzip입니다. Gzip은 파일 압축 및 압축 해제에 사용되는 파일 형식 및 소프트웨어 응용 프로그램이다. 데이터를 더 작은 크기로 압축하고 서버와 클라이언트 간에 데이터를 전송할 때 대역폭을 절약한다. 서버가 Content-Encoding 헤더가 "gzip"으로 설정된 응답을 보내면 클라이언트는 자동으로 gzip을 사용하여 응답의 압축을 푼다.
    다른 콘텐츠 인코딩 유형에는 gzip과 유사하지만 널리 지원되지 않는 deflate와 기본 콘텐츠 인코딩이며 데이터에 압축이 적용되지 않았음을 의미하는 identity가 포함된다.
    클라이언트가 Accept-Encoding 헤더가 있는 요청을 보낼 때 서버에 디코딩할 수 있는 콘텐츠 인코딩 유형을 알려준다. 그런 다음 서버는 가장 적절한 인코딩을 선택하고 해당 인코딩으로 설정된 Content-Encoding 헤더로 응답한다.
    콘텐츠 인코딩은 네트워크를 통한 전송을 위해 메시지 본문을 인코딩하는 방법을 지정하는 데 사용되는 전송 인코딩(Transfer-Encoding)과 다르다는 점에 유의해야 한다. 콘텐츠 인코딩은 메시지 본문의 데이터가 압축되는 방식을 나타내는 데 사용된다.

    gzip 적용 전

    gzip 적용 후

    gzip 적용 후 Size, Time이 모두 줄어든 거를 확인할 수 있다. (오차가 있을 수 있다.)

  3. Transfer-Encoding: 이 헤더는 서로 다른 유형의 시스템 간에 또는 서로 다른 네트워크 프로토콜을 통해 메시지를 안전하게 전송하기 위해 메시지의 페이로드에 적용된 인코딩 형식을 지정하는 데 사용되는 홉별 헤더이다. 이는 전송 중에 크기를 줄이기 위해 메시지의 페이로드에 적용된 압축 유형을 설명하는 메시지 수준 헤더인 Content-Encoding과 다르다. Transfer-Encoding은 서로 다른 유형의 시스템 간에 또는 서로 다른 네트워크 프로토콜을 통해 메시지를 안전하게 전송할 수 있도록 하는 메커니즘인 반면 Content-Encoding은 전송 중에 메시지의 페이로드 크기를 줄이는 메커니즘이다.

CORS랑 관련된 옵션

CORS(Cross-Origin Resource Sharing)는 웹 페이지가 페이지를 제공한 도메인이 아닌 다른 도메인에 대한 XMLHttpRequests를 만들 수 있도록 하는 메커니즘이다. HTTP 헤더는 브라우저에서 CORS 동작을 제어하는 데 사용됩니다.

XMLHttpRequests(XHR) 은 클라이언트 측 스크립트가 서버에 HTTP 요청을 할 수 있도록 웹 브라우저에서 제공하는 웹 API이다. 이를 통해 웹 페이지는 전체 페이지를 다시 로드하지 않고도 동적으로 콘텐츠를 업데이트할 수 있다.

  1. Access-Control-Allow-Origin: 이 헤더는 리소스에 액세스할 수 있는 원본을 지정하는 데 사용된다. 값은 단일 오리진 또는 오리진 목록으로 설정할 수 있다.

  2. Access-Control-Allow-Methods: 이 헤더는 리소스에 액세스할 때 허용되는 HTTP 메서드를 지정하는 데 사용된다. 이 값은 쉼표로 구분된 HTTP 메서드 목록으로 설정할 수 있다.

  3. Access-Control-Allow-Headers: 이 헤더는 리소스에 요청할 때 허용되는 헤더를 지정하는 데 사용된다. 값은 쉼표로 구분된 헤더 목록으로 설정할 수 있다.

  4. Access-Control-Allow-Credentials: 이 헤더는 자격 증명 플래그가 true일 때 요청에 대한 응답을 노출할 수 있는지 여부를 나타내는 데 사용된다. 쿠키, 인증 헤더 또는 TLS 클라이언트 인증서로 요청할 때 사용된다.

  5. Access-Control-Expose-Headers: 이 헤더는 클라이언트에 노출되는 헤더를 지정하는 데 사용된다. 기본적으로 간단한 응답 헤더만 노출된다.

  6. Access-Control-Max-Age: 이 헤더는 실행 전 요청 결과를 캐시할 수 있는 시간(초)을 지정하는 데 사용된다.

에러 해결기

A 클라이언트에서 뒤로 가기를 눌러서 B 클라이언트로 갔을 때 Access-Control-Allow-Origin이 아직 A 클라이언트를 바라보고 있어 CORS 에러가 나오고 있었다.

CORS 옵션 ?

먼저 CORS 옵션을 통해 해결할 수 있는지 확인해 보았다.

브라우저가 원본 간 요청의 경우에도 요청에 쿠키 및 인증 헤더를 포함하고 이를 통해 서버는 클라이언트를 인식하고 인증된 세션을 추적할 수 있기 위해서는 클라이언트에서 withCredentialstrue 로 설정해야 한다.

withCredentials 플래그는 일반적으로 페이지를 제공하는 도메인이 아닌 다른 도메인에 요청할 때 사용된다. 기본적으로 브라우저는 보안상의 이유로 교차 출처 요청에 자격 증명을 포함하는 것을 허용하지 않는다.
그러나 이 플래그를 true로 설정하면 요청에 쿠키 및 기타 자격 증명이 포함될 수 있으므로 다른 도메인의 리소스에 대한 인증된 액세스가 허용된다.
또한 서버는 Access-Control-Allow-Credentials: true와 같은 적절한 응답 헤더를 설정하여 출처 간 요청에서 자격 증명 사용을 허용해야 한다.

그러나 withCredentialstrue 로 설정된 경우 Access-Control-Allow-Origin 헤더는 * 를 사용할 수 없으며 단일 출처를 지정해야 한다. 이는 withCredentials 플래그가 쿠키, 인증 헤더 또는 TLS 클라이언트 인증서가 요청에 포함되어야 함을 나타내므로 서버가 이러한 민감한 정보를 수신할 수 있는 출처를 지정해야 하기 때문이다.

그러므로 CORS 옵션 설정을 통해서는 해결할 수 없었다.

a href ?

약간의 추측이 있습니다.

클라이언트에서 a href을 통해 페이지를 이동할 때는 CORS 에러가 나오지 않았다.

뒤로 가기를 눌렀을 때랑 a href을 통해 이동할 때랑 동작 방식의 차이가 있는지 찾아보았다.

캐싱 동작은 일반적으로 동일하다. 그러나 Chrome에서 뒤로 버튼을 사용하여 이전 페이지로 돌아갈 때 페이지가 브라우저의 캐시에 캐시될 수 있다. 페이지가 캐시된 경우 브라우저는 서버에 요청을 보내지 않고 대신 캐시된 페이지를 로드한다. 이렇게 하면 탐색 환경이 더 빨라질 수 있지만 캐시된 페이지가 업데이트되지 않은 경우 오래된 콘텐츠가 표시될 수도 있다.

이와 같은 차이가 있을 수 있다는 점을 알았다.

답은 캐싱

약간의 추측이 있습니다.

이 문제가 캐싱 문제임을 파악하고 캐싱 관련 옵션을 찾아보았다.

원티드

원티드의 기업 서비스로 채용담당자가 로그인하면 새로운 클라이언트로 이동될 거로 예상이 된다.

cache-control 옵션을 보면 no-cache, no-store 옵션을 사용한 거를 확인할 수 있다.

네이버 웹툰

네이버 웹툰의 페이지로 네이버를 누르면 새로운 네이버 클라이언트로 이동될 거로 예상이 된다.

cache-control 옵션을 보면 no-cache, no-store 옵션을 사용한 거를 확인할 수 있다.

해결

백엔드 코드

res.header('cache-control', 'no-store, no-cache');

해당 코드를 추가해서 Access-Control-Allow-Origin이 실제 origin을 바라보도록 할 수 있었다.

인프라로 해결하기

headers:
  response:
    set:
      - name: cache-control
        value: "no-store, no-cache"

이 코드를 추가하면 Istio Envoy에서 response 헤더를 no-store, no-cache로 설정할 수 있다고 한다.

추가 내용

그러나, 'cache-control', 'no-store, no-cache' 옵션을 추가하면 응답을 캐시하지 않고 요청될 때마다 서버에서 응답을 재검증하도록 클라이언트에 지시한다. 이것은 서버가 클라이언트로부터 더 많은 요청을 받을 수 있음을 의미한다.

'cache-control', 'no-store, no-cache' 옵션을 사용하는 클라이언트에서 공통적으로 확인할 수 있는 옵션이 Vary: Accept-EncodingContent-Encoding: gzip 이다.

이 2개의 옵션을 통해 위에서 설명한 거처럼 Size, Time을 최소로 줄이는 것이 필요해 보인다.

백엔드 코드

res.header('Vary', 'Accept-Encoding');

Vary: Accept-Encoding 추가

const express = require('express');
const compression = require('compression');

const app = express();

app.use(compression());

Content-Encoding: gzip 추가

인프라

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: gzip
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  httpFilters:
    - name: envoy.filters.http.lua
      typedConfig:
        '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
        inlineCode: |
          function envoy_on_response(response_handle)
            response_handle:headers():set("Content-Encoding", "gzip")
            response_handle:headers():add("Vary", "Accept-Encoding")
          end

이 코드를 추가하면 Istio Envoy에서 response 헤더를 Vary: Accept-EncodingContent-Encoding: gzip 으로 설정할 수 있다고 한다.

profile
Software Developer

0개의 댓글