CORS가 어려운 이유 - 5. CORS의 정의

CORS 에러는 웹 개발에 입문하는 많은 사람들에게 좌절을 안겨주는 유명한 오류입니다. CORS 에러는 웹을 이루는 다양한 요소를 먼저 이해하고 나면, 어렵지 않게 이해할 수 있습니다. 하지만 그 요소가 무척이나 많은데요. 필요한 개념들을 다양한 실습과 함께 모두 뜯어보고 이해해봅시다. [CORS 가 어려운 이유]는 시리즈로 구성됩니다. 아래 목차를 확인해주세요.

 

CORS 가 어려운 이유

  1. CORS 에러 마주하기
  2. HTTP 요청과 응답의 구조
  3. CORS 에러 발생 주체와 시점
  4. Origin과 Same-origin Policy
  5. CORS의 정의 톺아보기 (현재 글)

✅ "CORS? 그거 에러 아니야?" 하셨다면 한 번 읽어보시길 바랍니다.

CORS 에러 해결을 단순히 cors 라이브러리의 가이드를 단순히 따라 해결해오셨다면 한 번 읽어보시길 바랍니다.

✅ CORS 에러 해결을 단순히 응답 헤더에 `Access-Control-Allow-Origin: *`을 적용하여 해결해오셨다면 한 번 읽어보시길 바랍니다.

✅ ( 보다 엄밀한) CORS 의 정의를 알아봅니다.

CORS 에 활용되는 응답헤더 Access-Control-Allow-Origin 을 알아봅니다.

 

CORS 는 에러로 유명하지만, CORS 자체는 에러가 아닙니다. "교차출처간 상호작용을 서버의 응답헤더로 제어하는 방법"을 이르는 용어입니다. CORS 의 정의를 보다 상세히 알아봅니다. 또한, 현대 웹 개발은 프론트엔드 서버와 백엔드 서버가 나뉘어져 있어서 서로가 교차출처의 관계로 상호작용하는 경우가 많습니다. 이 경우에 CORS 에러 상황을 바르게 해결하는 방법은 무엇인지도 알아봅니다.

CORS 를 알아보기에 앞서

CORS 는 Cross-origin Resource Sharing 의 약자입니다. MDN 의 정의부터 살펴봅니다.

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

 

위 문장을 두가지로 구분해본다면 다음과 같습니다.

  • CORS 는 HTTP-header 기반의 매커니즘입니다.
  • CORS 매커니즘으로 브라우저에게 자원 불러오기를 허용해야하는 출처들을 서버에서 지시할 수 있습니다.

CORS 에러는 브라우저에서 발생합니다.

💡 이 섹션에서는 브라우저에 로드된 페이지(문서)가 외부 자원을 요청할 때만이 교차출처간 상호작용이 일어나고 CORS 에러가 발생할 수 있음을 설명합니다. 이를 충분히 이해하고 있다면 이 섹션을 건너뛰셔도 좋습니다.

 

웹브라우저로 사용할 때 일반적인 흐름은 다음과 같습니다.

  1. 웹브라우저가 서버에 요청을 보냅니다.
  2. 서버는 요청을 받아 요청에 따라 처리 결과를 응답하거나, 자원을 응답합니다.
  3. 웹브라우저는 응답을 받아 결과를 표시합니다.

CORS 에러는 위 과정에서 서버가 응답하고 브라우저가 결과를 표시하는 그 사이(2. 와 3. 사이)에 브라우저에서 발생합니다. 따라서, CORS 에러의 발생 여부를 서버는 알지 못합니다. 

 

(2장에서 자세히 다뤘었습니다.)

 

반응형

 

CORS 에러는 페이지가 교차출처의 자원을 요청할 때 발생합니다.

(재미로 보는) 역할극에 비유해보기

  • 사용자
  • 웹브라우저 (⬅️ 발암캐릭)
  • 서버A (A.com)
    • a.html (⬅️ 남자 주인공)
    • a.png
  • 서버B (B.com)
    • b.html
    • b.png
    • GET /hc (API) (⬅️ 여자 주인공)

CORS 를 이해하기 위해 먼저 이 역할극에 참여하는 등장인물(?)들은 위와 같습니다. (애틋한 사랑이야기입니다. 전쟁으로 남과 북으로 헤어져 연락이 닿지 않던 두 연인...)

  1. 사용자는 브라우저에 A.com 을 입력하였습니다.
  2. A.com 서버는 a.html 을 응답하였습니다.
  3. 브라우저는 a.html 을 화면에 표시하기 시작합니다.
  4. a.html 의 <script> 태그에 fetch('B.com/hc') 가 포함되어 있는 것을 확인합니다.
  5. 브라우저는 B.com/hc 에 GET 요청을 보냅니다. (요청헤더에 Origin: A.com 을 포함)
  6. B.com 은 (살아있다는 의미로) 성공 상태를 응답합니다. (응답헤더에 Access-Control-Allow-Origin 없음 - 아래서 자세히)
  7. 브라우저는 응답을 받습니다.
  8. 브라우저는 응답된 자원을 a.html 가 활용하도록 허용할지 제한할지 판단합니다; SOP 정책에 따라 CORS 를 발생시켜 a.html 이 결과를 처리할 수 없게 합니다.
💡 CORS 에러가 개발자를 괴롭힌다는 의미를 담아 '웹브라우저'를 빌런처럼 표현해보았습니다. 하지만 이것이 유효한 설정만은 아닐 것입니다. 빌런의 역할(발암캐릭)을 A.com/a.html 이 담당하게 하거나, B.com/hc 가 담당하게 하는 상황들도 보안 관점에서 유의미한 설정일 수 있습니다.

직접 요청

브라우저에 url 을 입력하거나, 즐겨찾기를 클릭하거나, 링크를 클릭하여 url 의 이동이 발생하거나, 새 탭이 열리는 상황에서 발생하는 요청에서는 CORS 에러가 발생하지 않습니다. 이는 모두 '사용자'에 의해 직접 발생한 요청이며, 상호작용할 두 개의 출처는 없습니다.

브라우저 상호작용 (resource fetching)

요청에 의해 응답된 html 문서에는 브라우저가 보여주어야 할 외부 자원들이 url 로 표기되어 있습니다. 브라우저는 이 자원들을 다시 로드합니다. 브라우저는 html 문서가 외부 자원을 요청했기 때문에 요청 주체를 현재 페이지로 간주합니다. 특히 CORS이 제한되어야 하는 경우(일반적으로 교차출처간 읽기요청)에는 요청에 '출처(Origin)'를 요청헤더에 포함하여 요청합니다. 이 때 서버의 응답에 특별한 언급(아래에서 자세히 다룰 CORS 응답헤더)이 없다면 브라우저는 해당 요청에 대해 CORS 에러를 발생시켜 페이지가 응답 결과를 활용할 수 없게 만듭니다.

 

앞선 글에서 브라우저에서는 CORS 에러가 나는 동일한 요청을 curl 요청을 보냈을 때 CORS 에러 없이 정상적으로 결과가 표시되는 것을 확인했었습니다. curl 프로그램은 응답을 분석하여 응답에 포함된 자원을 다시 요청하지 않기 때문입니다. 반면, 브라우저는 응답된 문서에 포함된 외부 자원을 다시 로드한다는 특징이 있습니다. 교차출처간에는 어떤 일이 일어날지 모르기 때문에, 브라우저단에서 '일반적으로 허용해도 괜찮은' 요청과 '일반적으로 제한하는 게 좋은' 요청을 구분해 정책으로 정해놓은 것이 동일출처정책(SOP)'입니다. 이것은 정책일 뿐, 허용과 제한 정책을 프로그래밍적으로 개발자가 제어할 수 있도록 방법을 만들어 두었는데 그 방법의 이름이 바로 교차출처자원공유(CORS) 입니다.

교차출처자원공유: CORS

역할극으로 시작하기

  1. 사용자는 브라우저에 A.com 을 입력하였습니다.
  2. A.com 서버는 a.html 을 응답하였습니다.
  3. 브라우저는 a.html 을 화면에 표시하기 시작합니다.
  4. a.html 의 <script> 태그에 fetch('B.com/hc') 가 포함되어 있는 것을 확인합니다.
  5. 브라우저는 B.com/hc 에 GET 요청을 보냅니다. (요청헤더에 Origin: A.com 을 포함)
  6. B.com 은 응답헤더에 `Access-Control-Allow-Origin: A.com`을 포함하여 성공 상태를 응답합니다. 
  7. 브라우저는 응답을 받습니다.
  8. 브라우저는 응답된 자원을 a.html 가 활용하도록 허용할지 제한할지 판단합니다; 브라우저는 SOP 에 의해 기본적으로는 제한되어야 하는 교차출처간 상호작용의 상황이지만 B.com 이 자원의 활용을 a.html 에게 허용했다는 것으로 보고 응답된 결과를 a.html(의 script)이 활용할 수 있게 합니다.
  9. script 의 fetch() 의 결과가 처리됩니다.

CORS 는 헤더를 제어하는 매커니즘

SOP 에 의해 제한되는 교차출처 상호작용이지만 개발자가 의도적(합법적)으로 자원 공유를 하도록 지시할 수 있습니다. 이 방법을 CORS 라고 합니다. CORS는 서버의 응답헤더를 제어하는 것입니다.

 

CORS 에러가 발생하는 조건들을 살펴보면 다음과 같습니다. (중요하기 때문에 계속해서 언급합니다.)

  • CORS 에러는 브라우저가 서버의 응답을 수신한 이후 브라우저가 발생시킵니다.
  • CORS 에러는 브라우저에 로드될(또는 로드된) 문서에서 외부 자원을 요청할 때 발생합니다.
  • CORS 에러는 (기본 SOP를 따를 때) 교차출처 읽기 상황에서 발생합니다. (교차출처 삽입, 교차출처 쓰기는 에러 대상 아님)
  • CORS 에러는 서버의 응답을 받고, 페이지가 그 결과를 활용하도록 전달하기 전에 발생합니다.

여기에 한 가지 조건을 추가합니다.

  • CORS 에러는 응답헤더에 포함된 Access-Control-Allow-Origin 의 값이 요청헤더의 Origin 과 일치하지 않으면 발생합니다.

이것이 CORS 에 의해 브라우저의 CORS 에러를 제어할 수 있는 조건입니다.

Access-Control-Allow-Origin

CORS 를 하기 위해 활용하는 응답헤더입니다. 이 응답헤더로 브라우저의 CORS 에러를 제어할 수 있습니다. 응답헤더의 값에는 `*` 또는 특정 Origin 을 1개를 포함할 수 있습니다.

 

Access-Control-Allow-Origin: <origin> | *

 

모든 출처 허용하기 1

`Access-Control-Allow-Origin: *`

 

서버가 브라우저에게: 지금 브라우저에 로드된 페이지의 출처가 어디든 지금 응답하는 자원의 활용을 허가한다.

 

모든 CORS 에러를 한 방에 해결할 수 있는 마법의 주문입니다. 어떤 자원(또는 API)의 응답에 `Access-Control-Allow-Origin: *` 을 적용했다는 것은, 해당 자원을 어떤 출처든 상관 없이 공개하겠다는 의미가 됩니다. 

 

httpbin.org 는 API 요청을 테스트해볼 수 있는 사이트입니다. 다음을 진행해보면서 응답헤더를 확인해봅니다.

  1. 브라우저로 https://httpbin.org/#/HTTP_Methods/get_get 페이지를 엽니다.
  2. 개발자도구를 열고 Get 부분의 Try out -> Execute 를 순서대로 클릭합니다.
  3. 네트워크 탭의 해당 요청의 응답헤더에서 ` Access-Control-Allow-Origin: * ` 를 확인합니다.

모든 출처 허용하기2

이전 '모든 출처 허용하기1'에서 뭔가 이상함을 눈치를 채신 분들이 있으시다면 먼저 박수를 드려봅니다. 👏 👏 👏 이전 과정은 동일출처간 요청입니다. httpbin.org 페이지가 브라우저에 로드된 상태에서 httpbin.org/get 으로의 요청이었기 때문입니다.

 

httpbin.org/get 으로 교차출처 읽기 요청이 되도록 다음의 과정을 진행합니다.

  1. google.com 으로 접속합니다.
  2. 개발자도구 콘솔을 열고 `fetch(https://httpbin.org/get)` 를 실행합니다.
  3. 요청헤더의 Origin 을 확인합니다.
  4. 네트워크 탭의 해당 요청의 응답헤더에서 `Access-Control-Allow-Origin: https://www.google.com` 를 확인합니다.
  5. (1. ~ 4.) 과정을 yotube.com 에서도 진행해봅니다.
  6. 네트워크 탭의 해당 요청의 응답헤더에서 `Access-Control-Allow-Origin: https://www.youtube.com` 를 확인합니다.

Access-Control-Allow-Origin 은 응답헤더입니다. 즉 서버가 제어하는 응답헤더인데 요청헤더 Origin 과 동일한 값이 Access-Control-Allow-Origin 응답헤더에 포함되어 있습니다. httpbin.org/get API 는 요청헤더 Origin 의 값을 기억해뒀다가, 응답할 때 Access-Control-Allow-Origin 헤더의 값에 그대로 포함하여 응답하는 것입니다.

 

이렇게 요청헤더의 Origin 헤더의 값을 그대로 응답헤더 Access-Control-Allow-Origin 의 값으로 지정하여 모든 출처에서의 자원 활용을 허용할 수 있습니다.

 

❓ 이렇게 사용하는 것과 asterisk(*)을 사용하는 것에 기능적, 정책적인 차이가 있을까요? 아시는 분이 계시다면 댓글로 정보 공유를 부탁드려봅니다.

특정 origin 만 허용하기

서버에서 `Access-Control-Allow-Origin: https://www.angel.com`  이라고 명시했다면,  이는 브라우저에서 https://www.angel.com 을 Origin 으로 하는 경우에 CORS 에러가 발생하지 않습니다.

여러 origin 허용하기 (동적 Access-Control-Allow-Origin 할당)

CORS 스펙상, Access-Control-Allow-Origin 에 여러 출처를 동시에 지정할 수는 없습니다. 만약 하나의 백엔드 서버(back.com)에 접근하는 두 개의 프론트엔드 서버(frontA.com, frontB.com)가 있고, 각각의 프론트엔드 서버와 백엔드 서버는 서로 모두 출처가 다른 상황을 예로 들 수 있습니다. 백엔드 서버가 응답을 프로그래밍적으로 제어할 수 있는 경우라면, Access-Control-Allow-Origin 을 서버에서 응답하기 전에 동적으로 지정해줄 수 있습니다. 허용할 출처를 배열타입에 준하는 자료구조로 관리하고, 요청헤더의 Origin 의 값이 해당 자료구조에 포함된다면 Access-Control-Allow-Origin 에 Origin 을 포함해줍니다. 그렇지 않다면 ccess-Control-Allow-Origin  헤더를 포함하지 않습니다. 의사코드는 다음과 같습니다.

 

const whiteList: string[] = ["front1.com", "front2.com"] 

string origin = request.getHeader("Origin")

if (origin is element of whiteList) {
  response.setHeader("Access-Control-Allow-Origin: " + origin)
}

// reponse
반응형

Designed by JB FACTORY