CORS(Cross Origin Resource Sharing)

5 분 소요


1. Same-Origin Policy

우선 동일 출처 정책(Same-Origin Policy)에 대해서 알아보자.

MDN Web Docs
동일 출처 정책은 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호 작용하는 것을 제한하는 중요한 보안 방식입니다. 동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여줍니다.

여기서 말하는 출처는 웹 페이지를 받아온 서버를 의미한다. 브라우저의 주소창에 보이는 서버 주소를 출처라고 생각하면 쉽다. http://example.com 출처의 서버에서 HTML 문서를 받았다고 가정해보자. 해당 HTML 문서에는 자바스크립트(javascript) 코드 혹은 이미지 태그(img)를 통해 동일(http://example.com) 서버 혹은 다른 서버(예를 들어, http://foo.com)로부터 필요한 리소스를 조회하는 코드가 있을 수 있다.

브라우저는 HTML 페이지를 http://example.com 서버로부터 받았으니 내부 스크립트에서 http://example.com 서버로 리소스를 요청하는 것은 정상적이라고 판단한다. 이를 동일 출처(same origin) 요청이라고 한다.

동일 출처는 규칙이 있다. 두 URL의 프로토콜(protocol), 호스트(host), 포트(port)가 모두 같아야 한다. 동일 출처에 대한 간단한 예시를 들어보자. 아래 표는 웹 어플리케이션이 http://store.company.com (기본 포트, 80) 프로토콜, 호스트, 포트 정보를 가질 때 동일 출처 정책 상에서 요청의 성공 여부이다.

URL 결과 이유
http://store.company.com/dir2/other.html 성공 경로만 다름
http://store.company.com/dir/inner/another.html 성공 경로만 다름
https://store.company.com/secure.html 실패 프로토콜 다름
http://store.company.com:81/dir/etc.html 실패 포트 다름 (http://는 80이 기본값)
http://news.company.com/dir/other.html 실패 호스트 다름

2. Cross Origin Resource Sharing

그러면 교차 출처 자원 공유(CORS, Cross Origin Resource Sharing)는 어떤 개념일까?

MDN Web Docs
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.

위에서 했던 가정을 바탕으로 설명을 이어가보자. 브라우저는 페이지를 http://example.com 서버로부터 받았는데 내부 스크립트에서 다른 서버(http://foo.com)로 리소스를 요청하니 이상할 따름이다. 이를 교차 출처(cross origin) 요청이라고 한다. 브라우저는 일단 교차 출처로 요청을 보냈기 때문에 이상함을 감지하고 에러를 발생시킨다.

브라우저는 초기 동일 출처 요청만 허용했다. 하지만 교차 출처 요청이 필요한 경우가 많아짐에 따라 교차 출처 자원 공유(CORS) 프로토콜을 만들었다. 이는 동일 출처 정책 때문에 발생하는 불편함을 해결하기 위해 예외 상황을 열어두는 정책이다. 브라우저가 다른 출처의 리소스를 가져올 때 HTTP 응답 헤더에 올바른 CORS 헤더 정보가 포함되어 있으면 동일 출처가 아니더라도 이를 허용하는 정책이다. 브라우저가 올바른 CORS 헤더 정보를 받지 못하면 이는 CORS 정책 위반이므로 받은 응답을 사용하지 않는다. 리액트(react), 뷰(vue) 같은 싱글 페이지 애플리케이션을 개발한다면 로컬 환경에서 웹 서버와 API 서버를 동시에 실행할 때 CORS 문제를 자주 마주쳤을 것이다.

아래 실행 흐름을 통해 동일 출처와 교차 출처 요청의 차이점을 간단히 살펴보자. https://domain-a.com/https://domain-b.com/ 주소를 가진 서버가 두 개 존재한다.

  1. 브라우저 주소 창에 https://domain-a.com/가 입력되면 브라우저는 웹 서버에서 HTML 문서, 스타일 파일, 스크립트 파일 등을 다운로드 받고 화면에 표신한다. 현재 브라우저가 보여주는 화면의 출처는 https://domain-a.com/이다.
  2. 브라우저가 화면에 보이는 첫 번째 이미지는 https://domain-a.com/image.png 주소에서 다운로드 받는다. 이는 프로토콜, 호스트, 포트 번호가 같으므로 동일 출처이다. 브라우저가 정상적으로 동작한다.
  3. 브라우저가 화면에 보이는 두 번째 이미지는 https://domain-b.com/image-b.png 주소를 통해 다운로드 받는다. 이는 호스트 정보가 다르므로 교차 출처이다.
    • 교차 출처 서버(https://domain-b.com)로부터 올바른 CORS 헤더 정보를 받으면 브라우저는 에러를 발생시키지 않는다.
    • 브라우저는 올바른 CORS 헤더 정보를 받지 못하면 해당 응답을 버리고 CORS 정책 위반 에러를 발생시킨다.

3. How to work CORS?

간단하게 CORS 작동 방식을 정리해보자. 일부만 정리하였으며 구체적인 내용은 교차 출처 리소스 공유(CORS)에서 확인할 수 있다.

3.1. Simple Requests

단순 요청(simple request)은 아래 조건들을 만족해야 한다.

  • 메소드(method)
    • GET
    • HEAD
    • POST
  • 허용되는 Content-Type 헤더
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 유저 에이전트(user agent)가 자동으로 설정한 헤더 외에 허용되는 헤더
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width

다음과 같은 시나리오로 동작한다.

  1. 브라우저는 https://foo.example 서버로부터 웹 페이지를 전달받는다.
  2. 브라우저가 전달받은 웹 페이지에서 https://bar.other 서버의 리소스를 호출한다. 브라우저는 자신이 보여주고 있는 페이지의 출처 정보를 헤더에 Origin: foo.example 값으로 함께 전달한다.
    • 브라우저는 https://bar.other 서버로부터 원하는 컨텐츠와 Access-Control-Allow-Origin: * 응답 헤더를 받는다.
    • 특정 도메인의 요청만 허용하려면 Access-Control-Allow-Origin: https://foo.example와 같이 허용할 도메인을 명시한다.
  3. 브라우저는 https://bar.other 서버로부터 전달받은 리소스를 화면에 보여줄 수 있다.
https://evan-moon.github.io/2020/05/21/about-cors/


다음과 같은 요청을 받는다.

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example

다음과 같은 응답을 받는다.

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]

3.2. Preflight Requests

사전(preflight) 요청은 단순 요청과 달리 먼저 OPTIONS 메소드를 통해 다른 도메인의 리소스로 HTTP 요청을 보내서 실제 요청이 전송하기에 안전한지 확인하는 방법이다. Cross-Site 요청은 유저 데이터에 영향을 줄 수 있기 때문에 미리 전송(preflighted) 해보는 것이다. 브라우저에 의해 자동으로 실행된다.

사전 요청이 동작하는 시나리오를 살펴보자. 아래 이미지에 포함된 도메인을 https://bar.other/으로 대체하여 설명한다.

  1. 브라우저는 https://foo.example 서버로부터 웹 페이지를 전달받는다.
  2. 브라우저가 전달받은 웹 페이지에서 https://bar.other/ 서버의 리소스를 위해 프리플라이트 요청을 보낸다.
  3. CORS 헤더와 정상 응답을 받으면 본 요청을 보낸다.
https://evan-moon.github.io/2020/05/21/about-cors/


다음과 같은 사전 이 전달된다.

  • 실제 요청에서 사용할 메소드
    • Access-Control-Request-Method: POST
  • 실제 요청에서 사용할 헤더 정보
    • Access-Control-Request-Headers: X-PINGOTHER, Content-Type
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

사전 요청을 보내면 서버로부터 다음과 같은 응답을 받는다.

  • 허용되는 도메인
    • Access-Control-Allow-Origin: https://foo.example
  • 허용되는 메소드
    • Access-Control-Allow-Methods: POST, GET, OPTIONS
  • 허용되는 헤더 정보
    • Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  • 프리플라이트 요청 결과를 캐시할 수 있는 시간
    • Access-Control-Max-Age: 86400
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

사전 요청을 통해 교차 출처 공유 정책이 위반되지 않음을 확인하면 본 요청을 보낸다. 본 요청이란 개발자가 서버로부터 필요한 리소스를 가져오기 위한 API 호출이다. 일반적인 HTTP 통신이 이뤄지지만, 단순 요청과 마찬가지로 응답 헤더에 CORS 허용에 관련된 헤더를 전달받는다.

POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache

<person><name>Arun</name></person>

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some XML payload]

CLOSING

CORS 정책 위반으로 개발에 불편함이 발생하는데, 이를 해결하기 위한 방법은 두 가지 있다.

  • 프론트엔드 웹 서버의 프록시 기능을 사용하여 교차 호출이 발생하지 않도록 우회
  • 백엔드 서비스에서 CORS 허용 헤더를 응답

아래 포스트들은 CORS 정책 위반이 발생하지 않도록 백엔드 서비스와 프론트엔드 서비스를 구현한 예제다. 자세한 내용은 관련 포스트를 참조하길 바란다.

RECOMMEND NEXT POSTS

REFERENCE

카테고리:

업데이트:

댓글남기기