매번 CORS
에 대해서 정리해야겠다는 마음만 가지고 있고 정리를 미루고 있었다.
최근 진행한 W
사의 기술 인터뷰에서 질문으로 나왔지만 명확한 답변을 하지 못하였고 현재 진행중인 사이드 프로젝트에서 발생하는 CORS
관련 에러도 명확하게 해결하지 못하였다.
이 글을 읽은 사람들은 필자와 같은 실수를 하지 않기를 바라면서 개발자들을 힘들게 하는 CORS
에 대해서 정리해보도록 한다.
SOP(동일 출처 정책) 란?
CORS
를 학습하기 위해서는 SOP
라는 개념에 대해서 먼저 학습해야 한다.
동일 출처 정책(same-origin policy)는 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식이다.
동일 출처 정책은 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여준다.
여기서 출처는 두 개의 URL의 프로토콜, 포트(명시하는 경우), 호스트가 모두 같아야 동일한 출처라고 말한다.
우리가 홈페이지에 접속하기 위해 http://www.example.com
이라는 값을 입력했다고 가정한 상황에서 허용되는 값과 허용되지 않는 값을 비교한 표다.
그림에서 알 수 있듯이 프로토콜
, 호스트
, 포트
가 완벽하게 일치하는 경우에만 동일한 출처라고 인식하며 Origin
이 같다고 브라우저는 인식한다.
웹 브라우저에서 동작하는 프로그램은 로딩된 위치에 있는 리소스에만 접근할 수 있다는 정책이다.
결국 개발자들을 힘들게하는 CORS
오류를 발생시키는 범인은 동일 출처 정책이라는 점을 인지해야 하며 CORS
는 Origin
이 다른 경우에도 요청할 수 있도록 하기위해 사용되는 개념이다.
CORS 란?
CORS
는 Cross-origin resource sharing 의 약자로 교차 출처 리소스 공유를 의미한다.
교차 출처 자원 공유는 웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으로부터 요청할 수 있게 허용하는 구조이다.
위키백과에 의하면 자원이 최초 서비스된 도메인 이외의 다른 도메인으로 요청할 수 있게 허용한다고 적혀있다.
결국 개발자들을 힘들게 하는 것은 CORS
가 아니라 SOP
(Same-origin policy)라고 하는 동일-출처 정책이다.
CORS
는 추가 HTTP 헤더를 사용하여, 어떠한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우에게 알려주는 방법이다.
웹 애플리케이션은 리소스가 자신의 출처(프로토콜, 도메인, 포트)와 다를 때 교차 출처 HTTP 요청을 실행한다. 아래의 예시를 살펴본다.
웹 애플리케이션은 domain-a.com
이라는 출처에서 실행 중이기 때문에 같은 출처인 domain-a.com
서버에게 전달하는 요청은 같은 Origin
이기 때문에 항상 허용되어 있다.
하지만 다른 출처인 domain-b.com
으로의 요청은 Origin
이 다른 Cross-origin
요청이기 때문에 CORS
체제를 통해 요청을 해야 한다.
동일 출처 정책에 대한 처리
웹 브라우저에서 http://sitea.com
으로 접속하여 자바스크립트 코드를 로딩하고 http://api.my.com
을 호출한다면 SOP
를 위반하게 되어 호출 에러가 발생한다.SOP
를 위반하지 않기 위해서는 아래와 같은 세가지 방법이 사용된다.
- 프록시를 이용하는 방법
일반적으로 SOP
는 웹 페이지를 호출하기 위한 요청의 주소와 호출된 페이지에서 호출하는 API 서버의 주소가 다르기 때문에 발생한다.
웹 페이지를 제공하는 서버와 API를 제공하는 서버의 앞단에 Nginx
와 같은 Reverse Proxy
를 구축하여 주소를 동일하게 하여 SOP
를 위반하지 않도록 구현할 수 있다.
하지만 프록시를 이용한 방식은 자사의 웹 사이트를 서비스하는 경우에는 사용이 가능하지만 일반적으로 백엔드 개발자가 프론트엔드 개발자에게 REST API
를 제공하는 경우에는 적절하지 않은 방식이다.
- 특정 사이트에 대한 접근 허용 방식
브라우저에서 실행되는 자바스크립트의 요청을 받는 API 서버에서 들어오는 모든 요청을 허용하는 방식이다.
예를 들어 클라이언트에서 HTTP
로 API
를 호출하였을 때 요청에 응답을 하면서 HTTP
헤더에 요청을 처리해 줄 수 있는 출처를 의미하는 Request Origin
을 추가한다.
위에 있는 그림으로 예를 들어 API
서버에서 sitea.com
의 요청을 전부 허용하고 싶다면 아래와 같이 헤더에 Access-Control-Allow-Origin
을 추가하면 된다.
Access-Control-Allow-Origin: sitea.com
특정 사이트를 지정하지 않고 모든 사이트의 요청을 허용하고 싶다면 아래와 같이 직접적인 주소 대신 *
을 사용하면 된다.
Access-Control-Allow-Origin: *
실제 요청을 가지고 예를 들어 본다.
아래와 같이 브라우저가 서버로 요청을 보냈다고 가정해본다.
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
서버는 요청 헤더의 Origin
값을 보고 https://foo.example
라는 Origin
으로 부터 서버는 요청을 받은 사실을 알게 된다.
서버는 요청을 처리하면서 응답 헤더에 Access-Control-Allow-Origin
에 *
또는 클라이언트의 요청 값으로 전달된 Origin
값을 입력해야 한다.
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: * or Access-Control-Allow-Origin: https://foo.example
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
Preflight
를 이용한 CORS 통제
프리플라이트 방식은 실제로 요청을 서버로 보내기 전에 HTTP 메서드 중 OPTIONS
메서드를 사용하여 다른 도메인의 리소스로 HTTP 요청을 보내서 실제 요청을 전송해도 안전한지 확인하는 방식이다.Cross-origin
요청 방식은 유저 데이터에 영항을 줄 수 있기 때문에 미리 미리 전송해서 확인하는 방법이다.
클라이언트의 요청을 살펴본다.
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('Ping-Other', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
POST
메서드와 함께 XML body 데이터를 전달하는 클라이언트의 요청이다.
표준 헤더가 아닌 Ping-Other
가 사용되었고 Content-Type
헤더에는 application/xml
가 사용되었다.
프리플라이트가 트리거되는 조건에 충족(조건은 하단 주의 글 참고)되기 때문에 이 요청은 사전 요청인 preflighted
처리가 된다.
preflight request
를 살펴보면 아래와 같다.
(예시에서 POST
요청에 Access-Control-Request-*
헤더가 포함되는 것으로 나오지만 실제로는 OPTIONS
메서드 요청에만 필요하다.)
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-Request-Method: POST
는 실제 요청에서는 POST
메서드가 사용될 것을 서버에게 알려준다.Access-Control-Request-Headers: X-PINGOTHER, Content-Type
은 실제 요청에서 헤더에 X-PINGOTHER
와 Content-Type
의 헤더가 사용될 것이므로 서버에서 요청을 수락할지 결정한다.
preflight
요청을 받은 서버는 아래와 같은 preflight response
를 전달한다.
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
클라이언트가 서버에게 POST
메서드가 사용될 것이라고 알렸기 때문에 서버는 POST
, GET
, OPTIONS
메서드가 허용되어 있다고 알려준다.
요청시에 전달된 X-PINGOTHER
와 Content-Type
헤더 또한 허용되어 있다고 클라이언트에게 전달하게 되면서 클라이언트와 서버는 실제 요청을 주고받을 준비가 된다.Access-Control-Max-Age: 86400
은 서버의 응답을 캐싱할 수 있는 최대 캐싱 시간이다.
실제 요청은 CORS
를 처리하는 것과 크게 관련이 없으므로 다루지 않는다.
주의
아래의 조건을 충족하지 않는 경우 CORS
프리플라이트 요청이 트리거되지 않는다.
프리플라이트 요청이 자동으로 트리거되지 않는 경우 브라우저는 SOP
를 위반했다고 판단하고 CORS
에러를 발생시키며 사용자에게 응답 데이터를 제공하지 않는다.
GET
,HEAD
,POST
메서드 중 하나를 사용해야preflighted
처리된다.Connection
,User-Agent(en-US)
, Fetch 명세에서 "forbidden header name"으로 정의한 헤더, Fetch 명세에서 "CORS-safelisted request-header"로 정의된 헤더 이외의 헤더를 사용해야 한다.
단, Content-Type 헤더의 경우application/x-www-form-urlencoded
,multipart/form-data
,text/plain
이외의 헤더를 사용해야preflighted
처리된다. (Fetch 명세 문서는 글의 가장 하단부에 첨부한다.)
기타. 인증정보를 포함한 요청
기본적으로 Cross-origin
요청에서 브라우저는 자격 증명을 서버로 전달하지 않는다.
따라서 클라이언트에서 아래와 같이 withCredentials
값을 true
로 변경하여 자격 증명을 서버로 전달할 수 있도록 설정해야 한다.
const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain() {
if (invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
아래는 클라이언트 요청이다.
GET /resources/credentialed-content/ 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
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2
쿠키 값에 컨텐츠를 대상으로 하는 쿠키가 포함되어 있는 것을 확인할 수 있다.
쿠키 값이 서버로 전달되는 경우 서버는 정상적으로 클라이언트에게 응답 값을 표시하기 위해 아래와 같이 Access-Control-Allow-Credentials
값을 true
로 하여 헤더에 추가해야 한다.
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
단, CORS
실행 전 요청에는 자격 증명이 포함되지 않아야 한다.
서버에서 자격 증명으로 실제 요청을 처리할 수 있음을 의미하는 Access-Control-Allow-Credentials
값을 true
로 응답한 경우에만 요청에 자격 증명을 포함해야 한다.
또한 자격 증명 요청에 응답하는 경우 Access-Control-Allow-Origin
헤더 값에는 *
와 같은 와일드카드를 사용할 수 없다.
반드시 정확한 Origin
인 http://foo.example
와 같은 값을 헤더에 전달해야 정상적으로 컨텐츠가 브라우저에 표시된다.
지금까지 CORS
에 대해서 알아보았다.
이번 장에서는 CORS
의 이론과 처리 방식에 대해서 알아보았고 다음 장에서는 실제로 우리가 접할 수 있는 환경에서 어떻게 처리해야 하는지에 대해서 알아본다.
참고한 문서
'Infrastructure > Network' 카테고리의 다른 글
[Network] CORS 해결 예제 (0) | 2022.06.21 |
---|---|
[HTTP] 헤더 - 4 (쿠키) (0) | 2021.06.28 |
[HTTP] 헤더 - 3 (일반 정보) (0) | 2021.06.28 |
[HTTP] 헤더 - 2 (전송 방식) (0) | 2021.06.28 |
[HTTP] 헤더 - 6 (조건부 요청) (0) | 2021.06.28 |