요약 : html 문서 상단에 서버의 데이터에 접근 할 수 있는 대상을 명시하는 것.
CORS는 특정 헤더를 통해서 브라우저에게 한 출처(origin) 에서 실행되고 있는 웹 애플리케이션이 다른 출처(cross-origin)에 원하는 리소스에 접근할 수 있는 권한이 있는지 없는지를 알려주는 매커니즘
CORS는 Cross-Origin Resource Sharing의 약자로, 우리말로 교차 출처 리소스 공유로 번역되는데, 브라우저(origin)에서 다른 출처(cross-origin)의 리소스를 공유하는 방법이다.여기서 origin이란 특정 페이지에 접근할 때 사용되는 URL의 Scheme(프로토콜), host(도메인), 포트를 말한다. 그래서 same-origin이란 scheme(프로토콜), host(도메인), 포트가 같다는 말이며, 이 3가지 중 하나라도 다르면 cross-origin이다.
HTTP 요청에 대해서 HTML은 Cross-Origin 정책을 따르기 때문에 기본적으로 Cross-Origin 요청이 가능하다. link, img 등의 태그에서 다른 리소스에 접근하는 것이 그 예 이다. 하지만 Script 태그 내에 있는 요청(XmlHttpRequest, Fetch Api)에 대해서는 기본적으로 Same-Origin 정책을 따르고 있기 때문에 Cross-Origin 요청이 불가능하다.
script 내부에서 cross-origin 정책을 허용하지 않는 것이 초기에는 보안을 위해서 좋은 방법이라 생각되었지만 요즘은 대규모 웹 서비스가 늘어나며, 외부 호출에 대한 니즈가 점차적으로 많아지게 되었다. 그래서 많은 개발자들이 JSONP와 같은 우회적인 방법으로 cross-origin 정책을 회피하였다. 이에 W3C는 조금 더 안전하게 브라우저와 서버간에 교차 통신을 할 수 있도록 CORS라는 정책을 내놓았다.
다른 출처의 출처가 무엇인지 살펴봐야 하는데, 출처가 무엇인지 알기 위해서 먼저 URL의 구조를 살펴보아야 합니다. URL 구조는 아래 그림과 같습니다.

프로토콜의 HTTP는 80번, HTTPS는 443번 포트를 사용하는데, 80번과 443번 포트는 생략이 가능합니다.
출처(Origin)란 URL 구조에서 살펴본 Protocal, Host, Port를 합친 것을 말한다. 브라우저 개발자 도구의 콘솔 창에 location.origin를 실행하면 출처를 확인할 수 있다.

같은 출처인지 다른 출처인지 이해를 돕기 위해 예제를 하나 살펴보도록 하겠습니다. 현재 웹페이지의 주소가 https://beomy.github.io/tech/일 때 같은 출처인지 다른 출처인지 아래 테이블과 같은 결과를 얻을 수 있습니다.
| URL | 결과 | 이유 | |
|---|---|---|---|
| https://dbrrnjs9.github.io/about | 같은 출처 | Protocal, Host, Port 동일 | |
| https://dbrrnjs9.github.io/about?q=work | 같은 출처 | Protocal, Host, Port 동일 | |
| https://dbrrnjs9.github.io/about#work | 같은 출처 | Protocal, Host, Port 동일 | |
| http://dbrrnjs9.github.io 다른 출처 | Protocal | 다름 | |
| https://dbrrnjs9.github.io:81/about | 다른 출처 | Port 다름 | |
| https://dbrrnjs9.heroku.com | 다른 출처 | Host 다름 |
다른 출처의 리소스를 사용하지 못하게 하는것. 가끔씩 API를 호출할때 CORS policy오류가 발생하는 것을 확인 할 수 있는데 이는 브라우저가 동일 출처 정책을 지켜서 다른 출처의 리소스 접근을 금지하기 때문.
동일 출처 정책(Same-Origin Policy)를 사용함으로써 XSS나 XSRF 등의 보안 취약점을 노린 공격을 방어할 수 있다.
현실적으로 외부 리소스를 아예 참고하지 않을수는 없기 때문에 외부 리소스를 가져올 방법이 필요하다. 이 예외 조항이 CORS이다.
크롬이나 사파리 같은 웹 브라우저를 실해할 때 Command-Line 옵션을 통해서 Cross-Origin 서버로부터 받아온 리소스에 접근이 가능하도록 설정할 수 있다. 예컨데 크롬의 경우는 --disable-web-security 라는 명령어를 사용해서 실행하면 Cross-Origin 서버로부터 받아온 리소스에 접근이 가능하게 된다. 번외로 --disable-web-security 라는 명령어는 보안을 완전 무시하라는 명령어 같다. 사실 많은 사람들이 CORS를 보안적 매커니즘에 의해서 탄생했다고 생각하는데, 사실은 CORS는 보안에 반하는 정책이다.
서버로부터 응답받은 헤더에 "Access-Controller-Allow-Origin: *" 만 추가해주면 웹 브라우저는 Cross-Origin 서버로부터 받아온 리소스에 접근이 가능한 것이라고 판단한다. 이러한 작업을 플러그인을 통해서 서버 응답 헤더에 강제적으로 넣어줄 수 있는데, 대표적으로 구글 웹스토어에 Allow-Controll-Allow-Origin 플러그인이 있다. (일종의 속임수)
js나 css 같은 리소스들은 Cross-Origin 정책을 따르기 때문에 외부 요청이 가능하다. 이 점을 이용해서 Cross-Origin 정책을 우회한 방식이 있는데 바로 Jsonp 방식이다. 해당 포스팅에서는 범위에 벗어나는 내용이므로 생략.
W3C에서 내놓은 표준 정책 방식이다. 아래 참고
CORS의 동작 방식은 단순 요청 방법과 예비 요청을 먼저 보내는 방법 2가지 방법이 존재한다.
단순 요청 방법은 서버에게 바로 요청을 보내는 방법이다.

단순 요청은 서버에 API 요청하고, 서버는 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보낸다. 브라우저는 이 헤더를 확인하여 CORS동작을 수행할지 판단한다.
이때 브라우저가 판단하는 조건은 다음과 같다.
하지만 위 조건들은 1번을 제외하고는 많이 까다로운 조건이라고 한다.
아래와 같이 클라이언트에서 외부 서버로 요청을 보냈다면 어떠한 일이 발생하는지 Step by Step으로 알아보자.
const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';
xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();
이 경우 클라이언트가 서버에게 전송하는 내용을 살펴보고, 서버가 클라이언트에게 어떠한 내용을 전달하는지 살펴보도록 하자.
# 요청 헤더
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
Preflight 요청은 서버에 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법.

Preflight 요청은 실제 리소스를 요청하기 전에 OPTIONS라는 메서드를 통해 실제 요청을 전송할지 판단한다.
OPTIONS 메서드로 서버에 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보낸다. 브라우저는 단순 요청과 동일하게 Access-Control-Allow-Origin 헤더를 확인해서 CORS 동작을 수행할지 판단.
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Requset-Headers: X-PINGOTHER, Content-Type
# Prefight 요청 헤더
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: https://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
# Preflight 요청에 대한 응답 헤더
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 /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
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
추가적으로 Preflight Request 방식은 많은 리소스를 잡아 먹는다. 그렇기 때문에 서버에서 "Access-Control-Max-Age" 헤더 정보를 통해서 Preflight Request 를 캐싱함으로써 그 효율을 높힐 수 있다.
기본적으로 CORS 정책상 XmlHttpRequest 혹은 Fetch Api를 사용하여 외부 서버에 요청을 보낼 때 쿠키 정보나 HTTP Authentication 정보는 보내지 않는 것이 원칙이다. 그러나 특정 플래그 값을 이용해서 외부 서버에 쿠키 혹은 HTTP Authenticaion 정보를 보낼 수 있다.
만약 foo.example 서버에서 렌더링된 javascript에 아래와 같이 코드를 작성했다고 해보자.
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();
}
}
바로 7번째 라인을 통해서 간단하게 외부 서버로 쿠키 정보나 HTTP Authentication 정보를 보낼 수 있게 되었다. 위 요청을 보면 HTTP 메소드 중 GET 방식으로 외부 서버로 요청을 보내기 때문에 Prefight Request 방식을 사용하지 않지만, 만약 응답 받은 서버에서 "Access-Control-Allow-Credentials: true" 라는 헤더 값이 없다면, 브라우저는 응답 받은 데이터에 대해 무시할 뿐만 아니라 리소스에 접근할 수 없다.
# 요청 헤더
GET /resources/access-control-with-credentials/ 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
# 응답 헤더
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
참고로, 외부 서버로 요청할 때 withCredentials를 true로 세팅하여 보냈다면, 외부 서버로 부터의 응답 헤더에 Access-Control-Allow-Origin의 값에 WildCard가 오면 안 된다. 무조건 명시적으로 Origin의 URL정보가 있어야 한다.
JSONP(JSON with Padding)는 script요소가 외부 출처 리소스를 가져올 수 있는 특징을 사용하는 방법. 아래 코드와 같은 방법으로 사용할 수 있다.
<!-- Frontend -->
<!DOCTYPE html>
<html>
<script>
function jsonpFn (data) {
console.log(data) // beomy
}
</script>
<script
type="application/javascript"
src="http://localhost:3001/cors?callback=jsonpFn"
>
</script>
</html>
// Backend
router.get('/cors', (req, res, next) => {
res.send(`${req.query.callback}('beomy')`)
})
프론트엔드와 백엔드 사이에 프록시 서버를 두는 방법으로 CORS를 해결할 수도 있습니다. 개발 환경에서 CORS를 해결해야 한다면, Webpack Dev Server 등의 라이브러리를 사용해서 프록시 설정을 하는 방법도 있다.
출처:
https://vvshinevv.tistory.com/60
https://beomy.github.io/tech/browser/cors/