웹 개발을 하다 보면 CORS(Cross-Origin-Resource-Sharing)
정책을 마주치게 됩니다. 이걸 모른 채로 개발을 하다 보면 꽤나 당황스러운 경험을 하게 될지도 모릅니다. 분명히 정상적인 방법으로 요청을 보낸 것 같은데, 알 수 없는 에러를 뱉어내면서 결과를 보여주지 않을테니까요.
지금 보면 되게 당연한거지만...
Cross-Origin Request Blocked: The Same Origin Policy disallows
reading the remote resource at https://some-url-here. (Reason:
additional information here).
CORS를 지키지 않은 경우 브라우저가 어떻게 움직이는지 살펴보겠습니다. https://www.google.com
에 접속해 개발자 도구 콘솔을 열고 다음과 같이 입력해봅시다.
fetch('https://www.google.com')
요청이 잘 되네요. 그럼 이번에는 https://www.naver.com
에 접속해서 동일한 요청을 수행해 볼까요?
요청이 reject 되었다고 나오는 게 보입니다.
Basic concept of CORS
우선 CORS란, 위에도 적었듯 교차 출처 자원 공유
로 직역될 수 있습니다. 직역하니까 교차가 무슨 말인지 헷갈릴 수 있을 것 같아 조금 알기 쉽게 바꾸자면 다른 출처
가 될 수 있겠네요. 쉽게 말해서 다른 출처에서 자원을 사용할 수 있게 하는 정책
정도로 받아들이면 될 것 같습니다.
Origin?
그러면 출처(Origin)란 무엇인지에 관해서 궁금하실 겁니다. MDN의 정의를 살펴볼까요?
Origin request 헤더는 fetch가 시작되는 위치입니다. 경로 정보는 포함하지 않고 서버 이름만 포함합니다. POST requests에 포함되는 것처럼, CORS requests와 함께 전송합니다. Referer 헤더와 비슷하지만, origin 헤더는 전체 경로를 공개하지 않습니다.
Origin:<scheme> "://" <hostname> [ ":" <port> ]
대강은 알겠지만 여전히 이해하기 쉬운 말은 아닌 것 같습니다. 조금 쉽게 풀어서 설명을 해볼게요.
가령 우리가 Google에 접속한다고 가정을 해보겠습니다. https://www.google.com
이 도메인이 되겠네요. 이때 안녕이라는 문자를 검색해 볼까요?
주소창에 search
라는 경로가 추가되고, q=안녕
이라는 Query string이 추가된 걸 볼 수 있습니다. 이때, 출처는 Protocol과 Host, Port
까지를 말합니다. 일반적으로 주소창에 port가 노출되지는 않아요. 이 정보를 기반으로 다시 위의 주소를 살펴보겠습니다.
https://www.google.com/search?q=안녕
이 경우, https://www.google.com
만이 출처가 됩니다. 그 뒤의 /search?q=안녕
은 출처가 아닌 거죠. 위에서 설명한 포트의 경우, 보통 http는 :80
, https는 :443
을 사용하도록 되어있어 이것이 생략된 경우, 자동으로 해당 포트를 사용하는 것으로 간주됩니다. RFC 문서를 보면 해당 내용이 나와있어요. RFC문서는 IETF(Internet Engineering Task Force; 인터넷 국제표준화기구)에서 작성된 가이드입니다.
만약, 출처에 https://www.google.com:443
처럼 포트가 명시되어있다면, 이것까지 모두 일치해야만 같은 출처로 인정됩니다. 이 출처가 다르면 기본적으로는 CORS
와 SOP
에 의해 자원을 공유할 수도, 요청을 보낼 수도 없습니다.
SOP?
SOP는 Same-Origin-Policy
의 줄임말로, 동일 출처 정책
으로 번역됩니다. 말 그대로, 같은 출처에서 나온 자원끼리만 공유가 가능하다는 정책인데요, 현실적으로 Web을 구현할 때 다른 출처에서 자원을 받아다 사용하는 것을 모두 틀어막을 수는 없습니다. 하다못해 이미지라도 첨부하려면 다른 출처에서 나온 경로가 필요하니까요. 그래서 몇 가지의 예외
를 두었는데, 그것 중 하나가 CORS를 준수한 요청
입니다.
우리가 구현한 Web에서 어떤 요청을 보냈는데, 그게 동일 출처가 아니라면 무조건 SOP를 위반한 것이 되고, 그 와중에 CORS까지 준수하지 않았다면 해당 자원은 사용할 수 없게 되는 거죠.
몇 가지의 예외?
의외로 꽤 많은 예외가 있지만, 대표적으로는 다음과 같습니다.
<img>
로 표시하는 이미지<video>
,<audio>
로 재생하는 미디어- @font-face로 적용하는 폰트
- iframe으로 삽입하는 모든 것
여기서 추가적으로 IE에서만 적용되는 예외사항
이 있습니다.
IE... 너어는 진짜..
How to compare Origin?
위에 예로든 https://www.google.com:443
을 대상으로 어디까지가 동일 출처일까요?
https://www.google.com:80 // false: port 다름
http://www.google.com:443 // false: protocol 다름
https://www.google.com:443/search // true
https://api.google.com:443/search // false: Host 다름
https://www.google.co.kr:443/search // false: Host 다름
How CORS works?
http 요청에 대해서는 따로 글을 써야 할 만큼 복잡합니다. 그렇지만 최대한 간결하게 설명해볼게요. MDN에서는 세 가지 시나리오를 예로 들고 있습니다.
단순 요청 (Simple request)
한 번의 요청으로 처리가 종료되는 요청입니다. 이 경우 사전 요청 없이 본 요청을 보내고, 서버로부터의 응답 Header > Access-Control-Allow-Origin
값을 통해 브라우저가 정책 위반 여부를 검사합니다. 만일 이 값을 *(Wildcard)
로 설정한다면 모든 출처
에 대해서 허용한다는 뜻이 됩니다. 아무 때나 사용할 수 있는 건 아니고, 다음의 조건을 만족해야만 합니다.
GET
,HEAD
,POST
요청을 사용할 것- POST 요청인 경우,
Content-Type
이application/x-www-form-urlencoded
,multipart/form-data
,text/plain
일 것
- Header는 Custom header가 아닐 것
프리플라이트 요청 (Preflight request)
단순 요청에 해당하지 않는 거의 모든 시나리오가 포함됩니다. 이 경우 사전 요청(preflight)
과 본 요청
으로 나누어서 서버와 통신하게 됩니다. 사전 요청을 보내는 경우 브라우저는 OPTIONS Method
를 이용하는데, 거기에는 Origin 정보뿐만이 아니라 content-type
, method
등의 정보도 포함합니다.
사전 요청에 대한 응답 Header를 비교해서 본 요청을 보내게 되는데, 아마 서버와 브라우저 간에는 이런 대화가 오고 갈 겁니다.
브라우저: 나는
application/json
타입의POST
요청을 보낼 거야
서버: 나는www.foo-bar.com에서
보낸 요청만 허용하고 있어
만일 브라우저가 요청을 보낸 출처가 www.foo-bar.com
이라면 정상적으로 본 요청을 통해 자원을 얻어올 수 있을 것이지만, 그게 아니라면 브라우저는 정책을 위반했다고 판단해 에러를 돌려줍니다. 명심해야 할 것은, 이 경우에 에러를 보내는 주체는 서버가 아니라 브라우저입니다. 정책을 위반한 요청을 보냈더라도 서버의 응답은 200 OK
가 떨어진다는 거죠. 만약 요청에 대해 404 Not Found
로 응답이 돌아온다고 해서 그게 정책을 위반했다는 뜻은 아니니까요.
인증정보를 포함한 요청 (Credentialed request)
기본적으로 CORS 요청에는 쿠키나 인증 같은 credential은 전송되지 않습니다. 만약 자격 증명을 함께 전송할 수 있게 된다면, 사용자의 동의 없이 민감한 정보에 접근할 수 있을 테니까요. 그럼에도 불구하고 서버에서 사용하고 싶다면, credentials
옵션을 사용해야 합니다.
// client request
fetch(url, { credentials: 'same-origin' })
// server response header
..
Access-Control-Allow-Credentials: true
..
클라이언트에서 보내는 credentials 옵션에는 아래와 같은 값들이 사용 가능합니다.
만일 이것을 허용한다면 서버로부터의 Access-Control-Allow-Origin
헤더에는 Wildcard를 사용할 수 없고, 반드시 값을 지정해야 합니다.
TL;DR
- SOP는 동일 출처의 자원만을 사용할 수 있도록 한 정책이다
- CORS는 SOP의 예외 정책이다
- 정책 위반은 브라우저에서 판단한다
References
Javascipt Info - fetch와 Cross-Origin 요청
MDN - 교차 출처 리소스 공유 (CORS)