CORS 제대로 짚고 넘어가기


CORS란?

Cross-Origin Resource Sharing의 준말로, “교차된 출처에서 리소스가 공유 중”이라는 뜻이다.

조금 더 쉽게 풀어서 설명하면, “서로 다른 출처(Origin)에서 리소스(데이터)를 공유 중이다”를 의미한다고 볼 수 있다.

CORS Error

우리가 개발을 할 때, API 데이터를 받아오는 과정에서 가끔(?) 이러한 빨간 에러메시지가 뜨는 경우가 있는데, 이는 우리가 API 데이터를 불러오는 과정에서 CORS 정책을 위반했기 때문이다.

즉, 서로 다른 출처에서 리소스를 공유하는 오류를 범했다는 것이다.

뭐… 일단은 그렇다 치고…. 도대체 서로 다른 출처란 무슨 의미일까?


출처(Origin)

출처란, 리소스 및 어플리케이션 통신을 하는 주체의 주소다.

우리는 주로 브라우저를 통해 특정 사이트(url)에서 어떠한 서버에 데이터 요청을 보내고, 해당 서버로부터 데이터를 전달 받는다. 이 때에는 요청을 ‘보내는’ 클라이언트의 주소와, 요청을 ‘받는’ 서버의 주소가 필요한데, 이러한 주소들을 우리는 넓은 의미에서의 출처라고 부를 수 있을 것이다.

조금 더 직관적인 예를 들자면, 내가 누군가에게 전화를 걸 때에는 반드시 “나의 전화번호”와 “상대방의 전화번호”가 필요한 것과 같다.

origin 확인

만약 본인의 현재 출처가 궁금하다면, 아래와 같이 콘솔창에 location.origin을 입력하면 내 현재의 origin(출처)를 쉽게 알아낼 수 있다.

하지만, 정확하게 짚고 넘어가자면, CORS에서 출처란 프로토콜 + 호스트 + 포트 까지를 의미한다.

출처란?

위의 사진에서 볼 수 있듯이, 프로토콜(스킴) + 호스트(도메인) + 포트 까지가 하나의 출처다. 포트는 주로 생략되는 경우가 많은데, 프로토콜에 기본적인 포트 번호가 고정되어 있기 때문이라고 한다. 즉, 프로토콜에 포트번호가 담겨있는 것이다.

다시 본론으로 돌아와서, 이제 C-O-R-S에서 “O”는 뭔지 알겠다.

근데… 솔직히 여전히 감이 잡히질 않는다. “왜 서로 다른 출처(Origin)에서는 리소스를 공유하면 안되는 것일까?”

이에 대해 짚고 넘어가려면 SOP에 대한 학습이 선제되어야 한다.


SOP란?

Same-Origin Policy, 동일 출처 정책이다.

즉, 동일한 출처에서만 리소스가 공유될 수 있다는 정책인 것이다. 이는 2011년 RFC 6454에서 처음 등장한 보안 정책으로, 해당 문서에서는 아래와 같이 설명한다. (RFC는 국제 인터넷 표준화 기구(IETF)에서 관리하는 문서다)

RFC 6454,[3.5 Conclusion]

1
2
3
4
5
6
7

The same-origin policy uses URIs to designate trust relationships.
URIs are grouped together into origins, which represent protection domains.
Some resources in an origin (e.g., active content) are granted the origin's full authority, whereas other resources in the origin (e.g., passive content) are not granted the origin's authority.
Content that carries its origin's authority is granted access to objects and network resources within its own origin.
This content is also granted limited access to objects and network resources of other origins,
but **these cross-origin privileges must be designed carefully to avoid security vulnerabilities.**

요약하자면, 동일 출처를 갖는 경우 출처 간의 신뢰관계가 형성되어 리소스 접근에 대한 권한이 부여된다. 하지만 이러한 교차 출처에 대한 권한은 보안을 위해 신중하게 설계되어야 한다.

이렇듯, SOP에 의해 기본적으로 동일 출처에 한해서만 리소스가 공유될 수 있기 때문에, 다른 출처 간의 리소스 공유를 위해서는 최소한 CORS 정책을 따라야 한다. (보안 또 보안)

우리가 보통 개발을 할 때에는, localhost:3000 등에서 API 서버에 요청을 보낸다. 하지만 위의 사진처럼, 우리가 요청을 보내는 클라이언트의 출처와, 요청을 받는 서버의 출처는 다르다. 이 때문에 CORS 정책 위반 문제가 나타나며 데이터가 서버로부터 정상적으로 받아와지지 않는 것이다.

좋다. 우리는 이제 CORS에서 출처란 무엇이고, 왜 서로 다른 출처에서 리소스가 공유되면 안되는지에 대해서 다뤄보았다.

그럼 이제, CORS의 프로세스(동작 원리)에 대해 알아보도록 하자.


CORS는 어떻게 작동할까?

CORS 정책을 위반했는지 안했는지의 여부는 어떤 프로세스를 통해 도출되는 것일까?
이에는 세 가지 시나리오가 존재한다.

1️⃣ Preflight Request 방식

Preflight Request는 예비 요청이다. (가장 보편적인 방식)

예비 요청 예시 (preflight request)

핵심만 요약하여 설명하자면, 우선 브라우저가 본 요청 전에 예비 요청을 서버에 보낸다. 그리고 이 예비요청에 OPTIONS 메소드를 사용하여 “내 Origin은 이거고~, 난 본 요청에 이런이런 정보를 담아서 보낼거야!”와 같은 개괄적 정보들을 서버에 보내준다. 그러면 서버는 해당 예비 요청을 받고, Access-Control-Allow-Origin 이라는 액세스가 허용될 수 있는 출처가 무엇인지 해당 출처 정보를 응답으로 보내준다. 그러면 브라우저는 해당 응답 헤더의 정보를 통해 “나의 출처(Origin)가 Access-Control-Allow-Origin에 포함이 되는가?”를 체크하며 CORS 정책 위반 여부를 판단한다.

진짜 조금 더 쉽게 설명하자면…!!

쉽게 설명해본 예비 요청 프로세스

뭐 이런 과정이 예비 요청(preflight request)이라고 할 수 있는 것이다.

그리고 그 뒤에 브라우저가 자신의 출처(Origin)와 Access-Control-Allow-Origin의 출처들을 비교한 뒤, 자신의 출처가 이에 포함된다면 본요청을 보내는 것이다. 본 요청은 아래와 같다.

예비 요청 이후의 본 요청


2️⃣ Simple Request 방식

simple request 방식은 예비요청 없이 본요청을 바로 날려버리는 방식이라고 보면 된다.

간단히만 설명하도록 하겠다. 본 요청을 보내버리고 → 응답 헤더에 Access-Control-Allow-Origin가 담겨져 오면 → 비교 후 CORS 정책 위반 여부를 판별한다. 그게 끝이다.

하지만 단순 요청 방식을 보내기 위해서는 아래와 같은 조건들을 모두 충족시켜야 하기 때문에 굉장히 까다롭다고 한다.

[출처]CORS:Simple vs Preflight Request

1
2
3
4
5
6
7
8
9

#1 GET/HEAD/POST
If we manually set headers like

#2 Accept, Accept Language, Content-Language, Content-Type(refer below for more conditions )
If we set only the following Content Type

#3 application/x-www-form-urlencoded or multipart/form-data or text/plain
The above specifies the major conditions for a simple request, For more details please refer to the CORS articles or w3c CORS specification.

3️⃣ Credential Request 방식

서로 다른 출처 간의 데이터 통신에서 조금 더 보안에 신경을 쓰고싶을 때 사용하는 방식이다.

만약 내가 요청에 쿠키 또는 인증 정보 등을 담아서 서버에 요청을 보내주고 싶을 때?

⇒ 이 credentials 옵션을 사용하면 된다!

즉, fetch()를 사용할 때, 요청 주소 뒤에 credentials 옵션을 준 뒤, 쿠키 또는 인증 정보 등을 담아서 보낼 때, CORS 검사 조건을 조금 더 강화하는 것이라고 생각하면 될 것 같다. 기본적인 Credential Rquest 방식은 이하와 같다. (쿠키 또는 인증 정보가 들어가 있으면 보안이 더 강화되어야 하는 것은 당연하다)


  1. same-origin (default) : 동일한 출처 간의 요청에만 인증 정보를 담아 보낼 수 있다.

  2. include : 모든 요청에 인증 정보를 담아 보낼 수 있다.

    ⇒ 이 옵션을 설정한 경우, Access-Control-Allow-Origin : * 은 불가능하다. (origin 주소 값이 직접적으로 적혀 있어야 한다)

    ⇒ 응답 헤더에  Access-Control-Allow-Credentials: true가 있어야 한다.

  3. omit : 어떠한 요청에도 인증 정보를 담지 않는다.


이처럼, 서로 다른 출처에서 리소스를 공유하려고 하는데, 요청 정보에 쿠키 및 인증 정보와 같은 보안성이 필요한 정보가 담겨있다면?

이 때 브라우저는 CORS 정책 위반 여부를 조금 더 꼼꼼하고 깐깐하게 판별하는 것이다..!


CORS 해결 방법

대망의 CORS 해결 방법이다.

사실… 프론트엔드 개발자가 CORS 문제를 해결하는 것은 월권이라고 본다. 위에서도 확인했듯이, 결국 서버측에서 응답 헤더에 Access-Control-Allow-Origin 을 CORS 문제가 나지 않도록 잘 담아서 보내주면 되는 일이기 때문이다. 하지만, 그럼에도 CORS에 대해 확실히 짚고 넘어가보고자 해결 방법을 정리해보려 한다.

크게 세 가지의 해결 방식이 존재한다.


  1. 서버에서 Access-Control-Allow-Origin 응답헤더 추가
  2. Proxy Server 사용
  3. Chrome Extension 사용 (근본적 해결책 X)

Access-Control-Allow-Origin 응답헤더 추가

물론 Access-Control-Allow-Origin:*을 해주면 정말 쉽게 CORS 문제를 해결할 수 있을 것이다. 이는() 모~~~든 출처에 대한 액세스를 허용하겠다는 뜻이기 때문이다. 하지만, 대충 내용만 봤을 때도 이는 굉장히 보안에 취약한 방식일 것이라고 직감할 수 있다. 따라서, 뭐 개인적인 클론코딩 프로젝트, 테스팅 사이트 등이 아니라면… Access-Control-Allow-Origin:* 와 같은 전역 허용이 아닌, 출처 주소를 직접 명시하여 설정해주는 것이 바람직할 것이다.

Proxy Server 사용

만약 본인이 CRA를 통해 개발을 하고 있다면, 굉장히 해결 방법이 간단하다.

CRA는 기본적으로 package.json에 proxy 설정이 되어 있기 때문에, webpack-dev-server proxy 기능 활성화되어 있다. 따라서 아래와 같이 package.json에 proxy를 추가해주면 된다.

1
2
3
4
{
(생략...),
"proxy": "https://API Origin" // ex) https://play.google.com
}

이렇게 입력을 해주면, 이제 우리는 로컬 환경에서 fetch 또는 axios를 통해 GET요청을 보낼 때, https://localhost:3000/api와 같이 요청이 보내지는 것이 아닌, proxy에서 설정한 baseUrl을 출처 삼아 요청을 보내게 된다.

위의 작성 예시를 예로 들면, fetch(’/api’)를 했을 때 → https://localhost:3000/api가 아닌 → https://play.google.com/api로 요청이 가는 것이다!

하지만 만약 CRA를 통해 개발하고 있지 않다면 webpack-dev-server를 사용해줘야 한다.

설치

1
npm i webpack-dev-server -D

webpack config파일 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js

module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://원하는 baseUrl',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};

이렇게 하면 위의 CRA 경우와 같이, 웹팩이 프록싱을 해주며 CORS 문제를 해결해줄 수 있다!

크롬 익스텐션 사용

여러 익스텐션들

뭐… 이렇게 크롬 확장프로그램 웹스토어에 가서 원하는 것을 다운받아 설치해주면 된다. 하지만 이는 근본적인 해결책이 될 수 없는게, 나의 브라우저에서만 일시적으로 CORS가 허용되는 것이기 때문이다.


결론 및 정리

  1. 출처는 프로토콜(스킴) + 도메인(호스트) + 포트다.
  2. 요청 출처와 응답 출처가 서로 다르면 SOP를 위반한 것이다.
  3. 따라서 CORS 정책을 따라야지만 다른 출처에서도 데이터 요청 및 응답을 정상적으로 받을 수 있다.
  4. CORS 작동 프로세스에는, preflight, simple, credential 세 가지의 요청 방식이 존재한다.
  5. 핵심!! ⇒ 요청 Origin과 Access-Control-Allow-Origin이 같아야 한다.

참조한 자료

CORS는 왜 이렇게 우리를 힘들게 하는걸까?

RFC 6454 - The Web Origin Concept

CORS : Simple vs Preflight request


CORS 제대로 짚고 넘어가기

https://hoonjoo-park.github.io/cs/browser/CORS/

Author

Hoonjoo

Posted on

2022-03-01

Updated on

2022-03-01

Licensed under

Comments