쿼카러버의 기술 블로그

[웹 개발] CORS란 무엇인가? (what is Cross Origin Request Sharing?) (CORS 한번에 뿌시기) (nginx CORS에러) 본문

웹 개발

[웹 개발] CORS란 무엇인가? (what is Cross Origin Request Sharing?) (CORS 한번에 뿌시기) (nginx CORS에러)

quokkalover 2022. 1. 24. 23:32

CORS는 백엔드 개발을하든 프론트 개발을 하든 항상 부딪히게 되지만, 제대로 이해하지 않고 넘어가는 경우가 많았다.

이번 글을 정리하면서 CORS에 대해 최대한 쉽게 그리고 조금 더 깊이 있는 이해를 해보고자 한다. 추가로 Origin이 무엇인지에 대해서도 알아본다.  또한 nginx에서 cors 정책을 설정했는데도 에러가 발생하는 경우의 해결법도 간단하게 적어봤다.

CORS란?

CORS 이미지

CORS란 Cross Origin Resource Sharing의 약자로 브라우저의 현재 웹 페이지가 이 페이지를 받은 서버가 아닌 다른 서버의 자원을 호출하는 것을 의미한다. 웹 브라우저에서 외부 도메인 서버와 통신할 때 허락을 구하고 거절하기 위해 HTTP-header를 이용하는 메커니즘을 CORS라고 한다.

CORS에서 가장 헷갈리는 부분은 서버쪽에서 클라이언트를 대상으로 리소스의 허용 여부를 결정하지만, 막상 CORS 관련 에러는 서버쪽에서 보내는 에러가 아니라 브라우저가 사용자를 보호하기 위해 발생시키는 에러기 때문이다.

자 이게 무슨말인지 차근차근 정리해보자

  • CORS정책은 서버에 저장돼 있다. 즉, 저장된 CORS 정책을 브라우저에서 보내주는 일을 서버가 담당한다.
  • 하지만 CORS정책을 받아서 검증하는건 브라우저다.
    • 즉 브라우저는 HTTP요청을 할 때 CORS검증을 해야 하는 상황인지 판단하고,
    • 서버에게 응답받은 CORS검증 요청 결과에 따라서 해당 http요청을 실행하거나, 취소시키고 에러를 뱉는다.

이를 한 번 더 정리하면 Cross-origin요청을 하려면 서버의 정책을 확인하고, 서버의 정책이 허용되면, 브라우저에서 요청을 허락하고, 서버의 정책이 허용하지 않으면 브라우저에서 거절한다. 여기서 동의는 다른 origin으로부터의 응답이 올바른 CORS헤더를 포함한다는 것을 의미한다.

 

예시

웹 어플리케이션이 자신이 속한 origin에 있는 리소스를 요청하면 same-origin HTTP 요청을, 자기 자신이 속하지 않은 origin에 있는 리소스를 요청하면 웹 어플리케이션은 cross-origin HTTP요청을 실행한다.

Same Origin Request

http://www.abc.com/page/1 페이지에서 http://www.abc.com/api/products 로 ajax 요청이 발생할 경우 요청시 현재페이지와 요청 대상의 도메인이 같으므로 same origin request(동일 출처 요청)라고 할 수 있다.

Cross Origin Request

https://domain-a.com에서 https://domain-b.com/data.json으로 json 데이터 요청을 보낸다면 이게 cross-origin HTTP요청이 되는 것이다.

Same Origin Policy를 사용하는 이유

  • CORS가 등장한 이유는 Same Origin Policy에서 시작한다. SOP란 말 그대로 같은 Origin에서만 리소스를 서로 공유할 수 있는 정책이다.
  • 특정 Origin에서 문서 혹은 스크립트가 다른 Origin에서 가져온 자원과 상호작용하는 것을 제한한다.
  • Same Origin Policy를 적용하면 다른 Origin에서 자원 요청이 불가능해지기 때문에
    • 공격받을 수 있는 경로 제한
    • 해로운 문서들을 분리
    • 등의 이익이 있다.

Cross Origin Poilicy를 사용하는 이유

  • 클라이언트와 서버의 도메인을 따로 유지하거나
  • 외부 API를 연동하여 사용할때, App과 외부 API의 origin이 달라 자원 공유가 불가능한 상황 발생
  • 따라서 CORS를 적용해서 보안 문제들을 예방하고 내가 허용하는 Origin만 허용할 수 있게 된다.

Origin이란?

자 이제 origin에 대한 얘기를 많이했는데, origin이 무엇인지 파악하기 위해 아래 url의 구조를 한번 살펴보자

출처(Origin)이란 URL 구조에서 살펴본 Protocol, Host, Port를 합친 것을 말한다.

즉 cross origin = 다음 중에 하나라도 다른 경우에 발생한다.

  • 프로토콜 (http ≠ https)
  • 도메인 domain.com, other-domain.com
  • 포트 번호 (e.g. 8080 포트와 3000포트)

아래 비교표를 보고, https://etloveguitar.tistory.com에 대해서 COR인지 SOR인지 비교해보자

이 외에도 아래와 같은 경우도 Host불일치로 Cross-Origin이다.

http://example.com
http://api.example.com

Cors 동작 방식

Simple Request

  1. 브라우저에서 HTTP Header 에 Origin 속성에 요청을 보내는 Origin을 담아 서버에게 요청을 보낸다.
Origin: https://example.com
  1. 서버가 이 요청에 대한 응답을 할 때, 응답 Header의 Access-Control-Allow-Origin 에 '리소스를 접근하는 것이 허용된 Origin'를 담아 브라우저에게 응답을 보낸다. (도메인을 리턴하거나 다 허용하면 *)
  2. 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin 값을 비교한다.
  3. 두 개가 동일하다면 유용한 응답이라 판단한다.

CORS가 동작하는 방식은 아래 세 가지 시나리오에 따라 변경된다.

CORS를 처리하는 방법

CORS를 처리하는 방식은 크게 세 가지가 있다.

1) simple request

2) preflight request

3) credentialed request

1) Simple Request (단순 요청)

Simple Request는 Preflight Request와 다르게 요청을 보내면서 즉시 cross origin인지 확인하는데, 다음 조건을 모두 충족해야한다.

  • 메서드는 GET POST HEAD 중 하나여야 한다.
  • 헤더는 Accept, Accept-Language, Content-Language, Content-Type 만 허용
  • Content-Type 헤더는 다음의 값들만 허용
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

2) Preflight Request (프리 플라이트 요청)

preflight request란 실제 요청을 보내기 전에 실제로 요청하려는 경로와 같은 URL에 대해 서버에 OPTIONS메서드로 사전 요청을 보내고 요청을 할 수 있는 권한이 있는지 확인한다.

curl --location --request OPTIONS 'https://nonoexample.com' \
--header 'Origin: https://example.com' \
--header 'Referer: https://noexample.com' \
--header 'Access-Content-Request-Method: GET**'**

preflight Request 헤더에 들어가는 속성들

  • Origin: 요청을 보내는 페이지의 출처 (도메인)
  • Access-Content-Request-Method: 실제 요청하려는 메서드
  • Access-Content-Request-Headers: 실제 요청에 포함되어 있는 헤더 이름

preflight Response헤더에 들어가는 속성들

  • Access-Control-Allow-Origin: 요청을 허용하는 출처 ( * 와일드 카드이면 모든 곳에서 허용, 특정하려면 프로토콜 + 호스트 + 포트번호 입력)
  • Access-Control-Allow-Credentials: 클라이언트 요청이 쿠키를 통해서 자격 증명을 하는 경우에 true, true를 응답받은 클라이언트는 실제 요청 시 서버에서 정의된 규격의 인증값이 담긴 쿠키를 같이 보내야 한다.
  • Access-Control-Expose-Headers: 클라이언트 요청에 포함되어도 되는 사용자 정의 헤더
  • Access-Control-Max-Age: 클라이언트에서 Preflight 의 요청 결과를 저장할 기간을 지정, 클라이언트에서 Preflight 요청의 결과를 저장하고 있을 시간이다. 해당 시간 동안은 Preflight 요청을 다시 하지 않게된다.
  • Access-Control-Allow-Methods: 요청을 허용하는 메서드, 기본값은 GET, POST라고 보면된다. 이 헤더가 없으면 GET과 POST요청만 가능하다. 만약 이 헤더가 지정되어 있으면 클라이언는 헤더 값에 해당하는 메서드일 경우에만 실제 요청을 시도한다.Access-Control-Allow-Headers: 요청을 허용하는 헤더.

예시 :

Preflight Request는 요청을 예비 요청과 본 요청으로 나눈다. OPTIONS 메서드를 통해 다른 도메인의 리소스에 요청이 가능한지 (실제 요청이 전송하기에 안전한지) 확인 작업을 하고, 요청이 가능하다면 실제 요청을 보낸다. Cross-origin 요청은 유저 데이터에 영향을 줄 수 있기 때문에 Preflight 요청을 한다.

클라이언트와 서버간의 첫 번째 통신인 preflight request/response 를 좀 더 자세히 살펴보자.

 

Preflight Request

OPTIONS 요청과 함께 두 개의 다른 요청 헤더가 전송된다.

Access-Control-Request-Method:POST

첫 행은 실제 요청을 전송할 때 POST 메서드로 전송된다는 것이고,

Access-Control-Request-Headers: X-PINGOTHER, Content-type

두번째 행은 실제 요청을 전송 할 때 X-PINGOTHERContent-Type 사용자 정의 헤더와 함께 전송된다는 것을 서버에 알려준다.

 

Preflight Response

서버가 메서드와 헤더를 받을 수 있음을 알려준다.

Access-Control-Allow-Origin: http://foo.example    # 서버측 허가출처
Access-Control-Allow-Methods: POST, GET, OPTIONS # 허가 메서드
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type    # 서버측 허가헤더
Access-Control-Max-Age: 86400 # Prefilght 응답 캐시기간

마지막 Access-Control-Max-Age는 preflight request에 대한 응답을 캐시할 수 있는 시간(초)를 의미한다. 프리플라이트를 보내면 사전, 실제 요청 두 번이 매번 왔다갔다하기 때문에 브라우저가 캐싱을 해두고 똑같은 요청을 보낼 때 사전 요청을 보내지 않고 바로 본 요청을 보낸다.

위처럼 preflight request가 완료되면 실제 요청을 보낸다.

 

3) Credentialed Request (인증정보 포함 요청)

인증 관련 헤더를 포함할 때 사용하는 요청이다. 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest나 fetch API는 credentials옵션을 변경하지 않고서는 쿠키를 주고받을 수 없다. 기본적으로 쿠키 정보나 인증과 관련된 헤더를 요청에 담지 않기 때문이다.

따라서 credentialed request는 아래와 같은 세가지 옵션으로 보낼 수 있다.

  • omit : 쿠키를 전송하거나 받지 않는다.
  • same-origin : 동일 출처라면, users credentials(cookies, basic http auth)을 전송한다
  • include : cross-origin호출이라 할지라도 user credentials (cookies, basic http auth 등)을 전송한다.
fetch('주소', {
 credentials: 'include', // 모든 요청에 인증 정보 포함
});
axios.post(주소, 데이터, { withCredentials: true });

// 또는 공통으로 추가
axios.defaults.withCredentials = true;

CORS 정책 위반으로 에러 발생할 때 해결 방법

1) Access-Control-Allow-Origin 응답 헤더 세팅

  • 서버 측 응답에서 접근 권한을 주는 헤더를 추가하여 해결
'Access-Control-Allow-Origin': <origin> | *,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
'Access-Control-Max-Age': 10
  • 단일 출처를 지정하면 브라우저가 해당 출처가 리소스에 접근하도록 허용한다. 또는 자격 증명이 없는 요청의 경우 "*" 와일드 카드는 브라우저의 origin에 상관없이 모든 리소스에 접근하도록 허용한다.
  • GET, POST, PUT, DELETE, OPTIONS 메소드를 허용하고
  • Content-Type, Accept 헤더만 허용하며
  • 10초간만 캐슁해둔다

 

2) CORS모듈 사용

const cors = require("cors");
const app = express();

app.use(cors());

아무 옵션 없이 설정하면 모든 cross-origin 요청에 대해 응답하므로, 특정 도메인이나 특정 요청에만 응답하게 옵션을 설정하는 것이 좋다.

const options = {
  origin: "http://example.com", // 접근 권한을 부여하는 도메인
  credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
  optionsSuccessStatus: 200, // 응답 상태 200으로 설정
};

app.use(cors(options));
  • 특정 도메인만 접근 허용
app.get("/example/:id", cors(), function (req, res, next) {
  res.json({ msg: "example" });
});
  • 특정 요청에만 접근 허용

 

3) Dynamic하게 동일 origin으로 응답하도록 설정 (Credential 옵션이 include일 때)

실제로 이번에 메타데이터 서비스를 개발할 때 했던 경험인데, credentials 설정을 include/true 로 설정하면 CORS정책에 의해 Access-Control-Allow-Origin을 모든 출처를 허용하는 '*' 로 지정할 수 없다는 에러가 발생하며, 따라서 cors 설정에서 *을 입력하여 모든 출처를 허용한 경우에는 특정 출처를 정확히 명시해야 한다.

아래가 내가 받았던 에러메시지다.

Access to XMLHttpRequest at 'https://~~~' from origin 'https://~~~' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

원래는 CORS를 해결하려고 As-is처럼 설정했는데 만약 credentialed request옵션이 include인 경우에는 As-is처럼 *를 리턴할 경우에는 동작하지 않기 때문에 요청의 origin으로 응답하도록 설정해주어야 한다.

As-is:

  • Example 1:
    • 요청의 Origin: https://example.com
    • Access-Control-Allow-Origin 응답: *
  • Example 2:
    • 요청의 Origin: https://example2.com
    • Access-Control-Allow-Origin 응답: *

To-be:

  • Example 1:
    • 요청의 Origin: https://example.com
    • Access-Control-Allow-Origin 응답: https://example.com
  • Example 2:
    • 요청의 Origin: https://example2.com
    • Access-Control-Allow-Origin 응답: https://example2.com

 

 

Appendix : Nginx에서 CORS이슈 발생할 때

  • nginx.conf에서는 파일 사이즈의 기본값이 1m이하인 경우에만 CORS를 허용하도록 돼있다. 따라서 client_max_body값을 직접 설정해주면 CORS관련 설정을 한 뒤에 CORS에러를 방지할 수 있다.

 

참고자료 :

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

https://hanamon.kr/네트워크-http-options-메소드를-쓰는-이유와-cors란/

https://jeleedev.tistory.com/178

Comments