[CORS] CORS란? CORS 에러 해결하기

GY·2022년 3월 31일
10

Basic CS

목록 보기
24/28
post-thumbnail
post-custom-banner

Node.js와 Express를 공부했던 적은 없었지만, 이번 토이 프로젝트를 진행하면서 api를 구축해야했기 때문에 공부하면서 진행하고 있었는데, CORS 이슈에 맞닥뜨리게 되었다.

Access to fetch at 'urlA' from origin 'urlB' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

'urlB'의 'urlA'에서 fetch를 위한 액세스가 CORS 정책에 의해 차단되었다고 한다.
실행 전 요청에 대한 응답이 액세스 제어 검사를 통과하지 못했는데, 요청한 리소스에 'Access-Control-Allow-Origin' 헤더가 없기 때문이다.
정확하지 않은 응답이 요구 사항을 충족하는 경우 요청 모드를 'no-cors'로 설정하여 CORS가 비활성화된 리소스를 가져와야 하는 것 같다.

무슨 말인지, 하나하나 찾아보자.


👉 Origin이란?

출처리소스라는 키워드가 자주 등장한다.
출처란 데이터를 보내고 받는 각각의 웹사이트와 API의 주소를 말하며,
리소스는 주고받는 데이터를 말한다.

Origin은 간단하게 프로토콜, 주소, 포트번호의 한 쌍을 말한다.

즉 Origin을 따질 떄에는 Scheme, host, port number를 식별하여 이 3가지가 같으면 같은 Origin으로 인식한다.
Origin = [프로토콜]://[Host의 IP주소 또는 URL]:[포트번호]

이미지 출처

  • protocol: 통신규약, 사용자가 서버에 접속할 때 통신할 방식 정의
  • host: IP에 이름을 부여한 도메인 네임 혹은 서버 컴퓨터 IP
  • port: 포트번호. 한 개의 컴퓨터에는 여러개의 서버가 존재할 수 있는데, 포트번호로 어떤 서버에 접속할지 결정한다. 웹 서버는 전세계적으로 80번 포트를 이용하는 것이 표준이라 url에는 기본적으로 포트번호 80은 생략되어 있다.

Origin이 다르다는 말은,

  • HTTP, HTTPS 프로토콜이 다르거나
  • 주소가 다르거나
  • 포트번호가 다르다는 말이다.

👉 Same Origin Policy(SOP)

MDN의 정의에 따르면, SOP의 의미는 다음과 같다.

한 Origin으로부터 로드된 document 혹은 script가 
다른 Origin의 리소스와 상호작용 할 수 있는 방법을 제한하는 중요한 보안 메커니즘

즉 SOP는 보안 정책으로,대부분의 웹 브라우저는 SOP를 준수한다. 현재 사이트에서 다른 Origin에 요청한 것을 기본적으로 제한해 해커의 공격을 방어하는 것이다.

예를 들어, 해커가 사이트를 만들어 접속을 유도했다고 가정해보자.
이 사이트에 접속하게 되면 해당 사이트를 구성하는 js파일을 브라우저가 다운받는다.
해커가 js파일에서 사용자의 브라우저에 저장되어있는 토큰등의 정보를 얻어 개인정보 등을 요청해 받아갈 수 있다.

조금 더 자세히 살펴보자.

브라우저는 다른 출처끼리의 요청을 보낼 때 요청에 Origin이라는 항목의 헤더를 추가한다.
헤더에는 요청을 받는 곳의 IP 주소나 사용할 프로토콜 등의 요청에 대한 보충정보가 담긴다.
이 헤더의 Origin 항목에 들어가는 값은 앞서 설명한 3가지 - protocol, host, port 항목으로 구성된 값이다.

하지만... 예를들어, youtube의 iframe 태그를 사용해 영상을 임베드할 수 있는데, 이 때는 왜 제한이 되지 않을까?

특정 HTML Tag는 다른 Origin에서 오더라도 임베딩에 한해 가능하도록 허용해준다.
물론 이 때도 SOP가 적용되어 접근이 제한적이다.

그러나, 지금 필요한 것은 이러한 HTML Tag외에 다른 Origin으로부터 필요한 자원을 불러오는 것이다.


👉 CORS (Cross Origin Resource Sharing)

이전에는 JSONP등을 사용해 요청을 우회하는 방법을 사용하였으나, 합법적으로 리소스 공유를 할 수 있도록 만들어진 메커니즘이 CORS이다.

1. Cross Origin

상호간의 document 접근은 Origin이 다를 때 접근이 불가하거나 매우 제한적이다.

CORS는 다른 Origin의 데이터를 불러오고 싶을 때 CORS의 표준을 지켜 다른 Origin이더라도 허용해주도록 하는 것이다.


2. CORS는 어떻게 동작할까?

위에서 언급했듯, 웹 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용해 요청을 보내는데, 이 때 브라우저는 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.

Origin: https://evan-moon.github.io

이후 서버가 응답 할 때 응답 헤더에 Access-Control_Allow-Origin이라는 값에 혀용된 출처를 내려주고,
브라우저는 이 값을 비교한 후 안전한 요청으로 간주한다.

그리고 이 CORS가 동작하는 방식은 3가지의 경우에 따라 다르다.


1. Simple Request인 경우

단순요청이란 의미로, 요청이 simple request인 경우는 다음과 같다.

  • HTTP Method가 GET, POST, HEAD 중 하나일 때
  • Content-Type이 아래 중 하나일 떄
    - text/plain
    - application/x-www-form-urlencoded
    - multipart/form-data
  1. 브라우저가 받은 요청이 어떤 Origin에서 시작됐는지 헤더를 추가한다.
  2. 서버는 받은 CORS 요청이 유효한지 여부를 Access-Control-Allow-Origin 헤더로 응답해준다.

2. Preflight Request인 경우

Simple Request가 아닌 경우에 속하는데, 내 경우 put 메서드를 사용했고 Content-type 또한 'image/svg+xml'이었으므로 preflight request가 발생했다.

단순 요청이 아닌경우, 예를 들어 put, delete와 같은 메서드들은 서버의 데이터에 영향을 줄 수 있기 때문에 요청을 보내기전 허용 여부를 먼저 검증한다.

이 경우 브라우저는 요청을 한번에 보내지 않고 사전요청과 본 요청으로 나누어 서버로 전송한다.
이 본 요청 전의 사전요청을 preflight request라고 하는데, 이 예비 요청에는 HTTP 메서드 중 OPTIONS 메서드가 사용된다.

왜 사전요청이 필요할까?

사전 요청은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 역할을 한다.

이미지 출처

preflight request의 경우 브라우저에서는 본 요청을 보내기 전에 자동적으로 HTTP의 OPTIONS 메서드를 사용해 서버에 미리 예비 요청을 보낸다.

OPTIONS /products/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

서버는 이에 허용된 메서드와 헤더를 지정하여 응답한다.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Access-Control-Allow-Method: GET, POST, OPTIONS, PUT
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json

Access-Control-Allow-Origin / Method 등의 항목을 확인해
서버에서 허용되는 메서드와 출처를 포함한 요청일 경우, 브라우저는 본 요청을 다시 보내게 된다.

POST /products/ HTTP/1.1
Host: api.domain.com
Authorization: token
Content-Type: application/json
Origin: https://www.domain.com

CORS에러를 해결한 뒤 실제로 postman에서 options메서드를 사용해 요청을 보내 봤는데, 다음과 같은 응답을 받을 수 있었다.

Access-Control-Allow-Origin에 설정해둔 url이 들어가 있는 것을 볼 수 있었다.
허용할 출처리소스를 설정해두었으므로 브라우저에서 사전요청을 보냈을 때 응답헤더에서 이 항목을 찾을 수 있고, 이에 따라 요청이 안전함을 확인한 후 본 요청을 다시 보내게 되는 것이다.


👉 CORS Error 해결하기

클라이언트와 서버 양측에서는 각각 어떻게 이 에러를 해결할 수 있는지 알아보았다.
이번 프로젝트에서는 직접 Express로 서버를 만들었기 때문에 Server에서 해결할 수 있는 방법 2가지를 시도해 해결했다.


💻 1. Client: 프록시 서버 사용하기

프록시 서버란?
인터넷에서 유저를 대신해 데이터를 가져오는 서버로,
클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해준다.

요청해야 하는 URL앞에 프록시 서버 URL을 붙여서 요청하게 되면, 클라이언트에서 서버로 리소스를 요청할 때 발생하는 CORS문제를 간단히 해결할 수 있다.


💻 2. Client: 라이브러리 사용하기

http-proxy-middlewqre 라이브러리를 사용하면 로컬환경에 한해 CORS에러를 해결할 수 있는데,
해당 라이브러리가 요청 출처를 프록싱해준다.


⚙️ 3. Server: Access-Control-Allow-Origin 헤더 세팅

서버측에서 허용할 Origin을 Access-Control-Allow-Origin 응답 헤더에 넣어주면, 해당 Origin에서는 json 데이터와 같은 자원들을 응답하고 읽을 수 있다.

2가지 메서드를 사용할 수 있다.

  • response.setHeader() : 한 줄의 헤더를 추가
  • response.writeHead() : status code를 포함해 여러 항목을 헤더에 추가할 수 있음
router.put('/api/put/url', function (req, res) {
	res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');

⚙️ 4. Server: CORS 미들웨어 사용하기

Node.js 미들웨어 중 하나인 CORS를 사용할 수도 있는데, npm install cors명령어로 설치해준 뒤 다음과 같이 코드를 작성했다. 위 방법과 더불어 이 방법 또한 사용해보았다.

const cors = require('cors');
const corsOptions = {
	origin: 'url',
};

router.use(cors(corsOptions));

router.put('/api/put/url2', function (req, res) {
  //생략

수정한 API를 다시 배포한 뒤
클라이언트에서 다시 요청을 보내보면,

요청이 성공한 것을 볼 수 있다.


Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 1월 8일

좋은 글 감사합니다.

답글 달기