CORS(Cross-Origin-Resource Sharing) 란?

hoo00nn·2021년 1월 11일
10
post-thumbnail

CORS 란?

CORSCross-Origin Resource Sharing의 약자이다. 브라우저에서 다른 출처의 리소스를 공유하는 방법이다.

아래의 CORS policy 오류 메시지는 CORS 정책을 위반할 때 발생한다.

URL 구조

출처를 알기 위해선 URL 구조를 알아야한다.

프로토콜의 HTTP는 80번, HTTPS는 443번 포트를 사용하는데, 80번과 443번 포트는 생략이 가능하다.

출처(Origin) 란?

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

브라우저 개발자 도구의 콘솔 창에 location.origin를 실행하면 출처를 확인할 수 있다.

같은 출처 VS 다른 출처

같은 출처인지 다른 출처인지 확인하기 위해선 Protocol, Host, Port를 확인하면 된다.

만약 현재 웹페이지 주소가 https://velog.io 일 때 같은 출처인지 다른 출처인지 아래의 테이블을 보고 알 수 있다.

URL같은 출처이유
https://velog.io/writeOProtocal, Host, Port 동일
https://velog.io/write?id=1561ea92OProtocal, Host, Port 동일
https://velog.io/write#workOProtocal, Host, Port 동일
http://velog.io/writeXProtocal 다름
https://velog.heroku.io/writeXHost 다름
https://velog.io:81/writeXPort 다름

동일 출처 정책(Same-Origin-Policy)이란?

Postman으로 API를 테스트하거나, 다른 서버에서 API를 호출할 때는 멀쩡히 잘 동작하다가 브라우저에서 API를 호출할 때에만 CORS Policy 오류가 발생한 적이 있을거다. 그 이유는 브라우저가 동일 출처 정책을 지키고 있기 때문에 다른 출처의 리소스 접근을 금지하고 있기 때문이다. 즉, CORS는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다. 또한 CORS 정책을 위반하는 리소스 요청 때문에 에러가 발생했다고 해도 서버 쪽 로그에는 정상적으로 응답을 했다는 로그만 남기 때문에, CORS가 돌아가는 방식을 정확히 모르면 에러 트레이싱에 난항을 겪을 수도 있다.

동일 출처 정책의 장점

동일 출처 정책을 지키면 외부 리소스를 가져오지 못해 불편하지만, 동일 출처 정책은 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting) 등의 보안 취약점을 노린 공격을 방어할 수 있다. 하지만 현실적으로는 외부 리소스를 참고하는 것은 필요하기 때문에 외부 리소스를 가져올 수 있는 방법이 존재해야 한다. 외부 리소스를 사용하기 위한 SOP의 예외 조항이 CORS이다.

CORS 동작 원리

그럼 본격적으로 어떤 방법을 통해 서로 다른 출처를 가진 리소스를 안전하게 사용할 수 있는지 알아보도록 하자.

기본적으로 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.

Origin: https://velog.io

이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-Allow-Origin 이라는 값에 “이 리소스를 접근하는 것이 허용된 출처”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.

기본적인 흐름은 이렇게 간단하지만, 사실 CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경되기 때문에 여러분의 요청이 어떤 시나리오에 해당되는지 잘 파악한다면 CORS 정책 위반으로 인한 에러를 고치는 것이 한결 쉬울 것이다.

Simple Request

단순 요청은 서버에 API를 요청하고, 서버는 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보낸다. 브라우저는 Access-Control-Allow-Origin 헤더를 확인해서 CORS 동작을 수행할지 판단한다.

Simple Request 조건

아무 때나 단순 요청을 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있다. 게다가 이 조건이 조금 까다롭기 때문에 일반적인 방법으로 웹 어플리케이션 아키텍처를 설계하게 되면 거의 충족시키기 어려운 조건들이다.

  • 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
  • Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
  • 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.

사실 1번 조건의 경우는 그냥 PUT이나 DELETE 같은 메소드를 사용하지 않으면 되는 것 뿐이니 그렇게 보기 드문 상황은 아니지만, 2번이나 3번 조건 같은 경우는 조금 까다롭다.

애초에 조건에 명시된 헤더들은 정말 기본적인 헤더들이기 때문에, 복잡한 상용 웹 어플리케이션에서 이 헤더들 외에 추가적인 헤더를 사용하지 않는 경우는 거의 없고, 당장 사용자 인증에 사용되는 Authorization 헤더 조차 조건에는 포함되지 않는다.

3번 조건은 많은 REST API들이 Content-Type으로 application/json을 사용하기 때문에 지켜지기 어려운 조건이다.

Preflight request

프리플라이트(Preflight) 방식은 서버에 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법이다. 브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.

이때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 부르는 것이며, 이 예비 요청에는 HTTP 메소드 중 OPTIONS 메소드가 사용된다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.

우리가 자바스크립트의 fetch API를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내리면 브라우저는 서버에게 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내주게 된다.

이후 브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책을 비교한 후, 이 요청을 보내는 것이 안전하다고 판단되면 같은 엔드포인트로 다시 본 요청을 보내게 된다. 이후 서버가 이 본 요청에 대한 응답을 하면 브라우저는 최종적으로 이 응답 데이터를 자바스크립트에게 넘겨준다.

Credentialed Request

3번째 시나리오는 인증된 요청을 사용하는 방법이다. 이 시나리오는 CORS의 기본적인 방식이라기 보다는 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법이다.

기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다.

이 옵션에는 총 3가지의 값을 사용할 수 있으며, 각 값들이 가지는 의미는 다음과 같다.

옵션 값설명
same-origin (기본값)같은 출처 간 요청에만 인증 정보를 담을 수 있다
include모든 요청에 인증 정보를 담을 수 있다
omit모든 요청에 인증 정보를 담지 않는다

same-origin 은 기본 값으로, 같은 출처 간에 쿠키 등의 인증 정보 전달이 가능하다. include출처에 상관없이 모든 요청에 쿠키 등의 인증 정보를 전달할 수 있다. omit쿠키 등의 인증 정보를 전달하지 않는다.

만약 same-origin이나 include 와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin 만 확인하는 것이 아니라 좀 더 빡빡한 검사 조건을 추가하게 된다.

CORS 에러 해결 방법

앞에서 이야기 한 CORS 동작 원리를 보면, 서버에서 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보내는 방식으로 CORS 에러를 해결할 수 있다. 프론트엔드 개발자가 CORS 에러를 확인했다면, 서버에 Access-Control-Allow-Origin 등 CORS를 해결하기 위한 몇 가지 응답 헤더를 포함해 달라고 요청해야 한다.

Node.js의 Express는 cors라는 서드 파트 미들웨어를 지원한다. 이 라이브러리에서 CORS 응답 헤더를 추가해 주기 때문에, 개발자가 별도의 CORS 응답 헤더를 추가해 주지 않아도 된다.

HTTP 응답 헤더

Access-Control-Allow-Origin: | *

Access-Control-Allow-Origin 헤더에 작성된 출처만 브라우저가 리소스를 접근할 수 있도록 허용한다.

사용 방법

아래와 같이 응답 헤더가 작성되었다면 https://velog.io 페이지에서 브라우저는 서버 응답으로 온 리소스를 접근할 수 있다.

Access-Control-Allow-Origin: https://velog.io

아래와 같이 *(와일드 카드)가 작성되었다면, 브라우저는 출처에 상관없이 모든 리소스에 접근할 수 있다.

Access-Control-Allow-Origin: *

예시

아래 코드를 브라우저에서 실행하여 Access-Control-Allow-Origin 헤더를 처리하지 않은 서버에 API를 호출하게 되면,

fetch('http://localhost:3000/cors', {
  method: 'PUT',
}).then(function(response) {
}).catch(function(error) {
})

아래와 같은 에러가 발생한다.

서버에서 아래와 같이 응답 헤더를 추가해 주어야 한다. 와일드카드를 사용하여 모든 출처에서 리소스를 접근할 수 있도록 설정했다.

Access-Control-Allow-Methods: (method)[, (method)]*

Access-Control-Request-Method 헤더는 리소스 접근을 허용하는 HTTP 메서드를 지정해 주는 헤더다.

사용 방법

사용 방법은 아래 코드와 같다. Access-Control-Allow-Methods 헤더에 GET, PUT, POST, DELETE 등의 HTTP 메서드를 ,로 구분하여 넘겨주면 된다.

Access-Control-Allow-Methods: GET, PUT

예시

아래 코드를 브라우저에서 실행하여 Access-Control-Allow-Methods 헤더를 처리하지 않은 서버에 API를 호출하게 되면,

fetch('http://localhost:3000/cors', {
  method: 'PUT',
}).then(function(response) {
}).catch(function(error) {
})

아래와 같은 에러가 발생한다.

서버에서 아래와 같이 응답 헤더를 추가해 주어야 한다.

Access-Control-Allow-Origin는 *로 모든 출처를 허용한 상태이고, 브라우저의 요청 헤더에 포함된 Access-Control-Request-Method 헤더 값을 그대로 Access-Control-Allow-Methods 헤더에 추가해 주었습니다.

Access-Control-Expose-Headers: (header-name)[, (header-name)]*

서버에서 응답 헤더에 Access-Control-Expose-Headers를 추가해 줘야 브라우저의 자바스크립트에서 헤더에 접근할 수 있다.

사용 방법

아래와 같이 ,로 구분하여 여러 개의 헤더를 넣을 수 있다.

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

예시

서버에서 아래 코드와 같이 Access-Control-Expose-Headers 헤더에 X-Custom-hoo00nn 추가해 주고, X-Custom-hoo00nn 헤더에 값을 담아 응답을 하면,

app.options("/cors", (req, res, next) => {
  res.set("Access-Control-Allow-Origin", "*");
  res.set(
    "Access-Control-Allow-Methods",
    req.get("Access-Control-Request-Method")
  );
  res.send();
});

app.put("/cors", (req, res, next) => {
  res.set("Access-Control-Allow-Origin", "*");
  res.set('Access-Control-Expose-Headers', 'X-Custom-hoo00nn')
  res.set('X-Custom-hoo00nn', 'hoo00nn')
  res.json({ result: "success" });
});

브라우저에서는 아래 코드를 실행해서, X-Custom-hoo00nn 헤더 값을 가져올 수 있게 됩니다.

fetch('http://localhost:3000/cors', {
  method: 'PUT',
}).then(function(response) {
  console.log(response.headers.get('X-Custom-hoo00nn')) // hoo00nn
}).catch(function(error) {
})

서버에서 Access-Control-Expose-Headers: X-Custom-hoo00nn로 자바스크립트에서 접근할 헤더를 명시해 주지 않으면, 자바스크립트에서 X-Custom-hoo00nn 헤더 값은 undefined가 된다.

Access-Control-Allow-Headers: (header-name)[, (header-name)]*

Access-Control-Request-Headers 헤더는 브라우저에서 보내는 커스텀 헤더 이름을 서버에 알려주기 위해 사용한다.

사용 방법

자바스크립트에서 커스텀 헤더를 서버에 전달하게 되면, OPTIONS 요청 헤더의 Access-Control-Request-Headers 헤더에 커스텀 헤더 이름이 추가된다. 서버에서는 Access-Control-Request-Headers에 작성된 값을 보고 Access-Control-Allow-Headers 응답 헤더에 커스텀 헤더 이름을 명시해 주어야 한다.

Access-Control-Allow-Headers: X-Custom-Request

예시

아래 코드를 브라우저에서 실행하여, Access-Control-Allow-Headers 처리되지 않은 API를 호출하게 되면,

fetch('http://localhost:3000/cors', {
  method: 'PUT',
  headers: {
    'X-Custom-Request': 'hoo00nn',
  }
}).then(function(response) {
}).catch(function(error) {
})

아래와 같은 에러가 발생한다.

서버에서 아래와 같이 응답 헤더를 추가해 주어야 한다.

브라우저의 자바스크립트에서 X-Custom-Request 헤더에 hoo00nn 값을 서버에 전달하였고, 서버에서는 Access-Control-Allow-Headers 헤더에 Access-Control-Request-Headers 헤더 값을 저장하여 서버에서 X-Custom-Request 값을 사용할 수 있게 한 코드다.

Access-Control-Max-Age: (delta-seconds)

preflight 요청 결과를 캐시 할 수 있는 시간을 나타낸다.

사용 방법

아래와 같이 초 단위로 캐시 시간을 설정한다.

Access-Control-Max-Age: 60

위의 코드는 60초 동안 preflight 요청을 캐시 하는 코드다. 60초 동안 OPTIONS 메서드를 사용하는 예비 요청을 보내지 않는다.

예제

fetch('http://localhost:3000/cors', {
  method: 'PUT'
}).then(function(response) {
}).catch(function(error) {
})

Access-Control-Allow-Credentials: true

자바스크립트 요청에서 credentials가 include일 때 요청에 대한 응답을 할 수 있는지를 나타낸다. false로 설정해 주고 싶을 경우에는 헤더를 생략하면 된다.

사용 방법

Access-Control-Allow-Credentials: true

예제

아래 코드를 브라우저에서 실행하여, Access-Control-Allow-Credentials 처리되지 않은 API를 호출하게 되면,

fetch('http://localhost:3000/cors', {
  method: 'PUT',
  credentials: 'include'
}).then(function(response) {
}).catch(function(error) {
})

아래와 같은 에러가 발생한다.

서버에서 아래와 같이 응답 헤더를 추가해 주어야 한다.

브라우저는 인증 모드가 include일 경우, 모든 요청을 허용한다는 의미의 *를 Access-Control-Allow-Origin 헤더에 사용하면 안된다.

이처럼 요청에 인증 정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면 브라우저는 CORS 정책 위반 여부를 검사하는 룰에 다음 두 가지를 추가하게 된다.

  • Access-Control-Allow-Origin에는 *를 사용할 수 없으며, 명시적인 URL이어야한다.
  • 응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야한다.

그렇게 때문에 위의 코드처럼 Access-Control-Allow-Credentials: true 추가 뿐만 아니라, Access-Control-Allow-Origin 헤더도 *(와일드카드)가 아닌 출처를 명시해 주어야 한다.

다른 해결 방법

JSONP

JSONP(JSON with Padding)는 <script> 요소가 외부 출처 리소스를 가져올 수 있는 특징을 사용하는 방법이다. 아래 코드와 같은 방법으로 사용할 수 있다.

<!-- Frontend -->
<!DOCTYPE html>
<html>
  <script>
    function jsonpFn (data) {
      console.log(data) // hoo00nn
    }
  </script>
  <script
    type="application/javascript"
    src="http://localhost:3000/cors?callback=jsonpFn"
  >
  </script>
</html>
// Backend
app.get('/cors', (req, res, next) => {
  res.send(`${req.query.callback}('hoo00nn')`)
})

Proxy

프론트엔드와 백엔드 사이에 프록시 서버를 두는 방법으로 CORS를 해결할 수도 있다. 개발 환경에서 CORS를 해결해야 한다면, Webpack Dev Server 등의 라이브러리를 사용해서 프록시 설정을 할 수 있다.

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.hoo00nn.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
      },
    }
  }
}

/api 로 시작하는 URL로 보내는 요청에 대해 브라우저는 localhost:8000/api 로 요청을 보낸 것으로 알고 있지만, 사실 뒤에서 웹팩이 https://api.hoo00nn.com 으로 요청을 프록싱해주기 때문에 마치 CORS 정책을 지킨 것처럼 브라우저를 속이면서도 우리는 원하는 서버와 자유롭게 통신을 할 수 있다.

참고자료

profile
😀 신기술에 관심이 많고, 함께 성장하고 함께 개발하고 싶은 개발자가 되고 싶습니다. 😀

0개의 댓글