CORS


개요

현재의 Web 브라우저에서는 하나의 Web 사이트가 가진 정보가 다른 악의적인 Web 사이트에 악용되는 것을 방지하기 위해 Same-Origin Policy(동일 출처 정책)이 적용된다.

예를 들어, 각각 다른 도메인으로 된 백 엔드 API와 프런트 엔드 간에 통신하여 자원을 요청하게 되면 Origin(도메인, 프로토콜, 포트 번호)이 다르므로 에러가 발생한다는 것이다. 즉, Web 사이트 https://api.devkuma.com/ 를 브라우저에서 표시 할 때, 이 Web 페이지에서 XMLHttpRequest(이하 XHR)와 Fetch API에서 다른 Web 사이트 https://www.devkuma.com/ 에서 HTTP(S)에서 데이터를 읽으려고 하면 오류가 발생한다.

그런데, 사용자가 액세스한 악의적인 Web 사이트라면 몰라도 데이터 연계를 하는 상대로서 신뢰 관계 수있는 Web 사이트까지 제한을 걸어 버리면 불편하므로 데이터 액세스를 허용 할 수 있는 Web 사이트에 대해서는 Origin이 달라도 액세스를 가능하게 하기 위해 CORS(Cross-Origin Resource Sharing) 라는 것이 필요하다.

CORS는?

  • Cross-Origin Resource Sharing 약자로 오리진 간의 자원 공유의 의미한다.
  • 브라우저는 서로 다른 출처간에 통신을 원칙적으로 금지하고 있다. 그러나 Cors 설정을 하여 다른 오리진 간에도 통신할 수 있게 된다.
  • 브라우저에서 보고있는 페이지와는 다른 도메인에서 데이터의 받아오는 것을 허가하는 방식이다.
  • 브라우저는 보안 크로스 사이트 스크립팅을 방지하기 위해 사용된다.
  • 오리진 도메인의 서버와만 통신 할 수 없는 제약이 있다.

Origin(오리진)?

CORS에 대해서 좀 더 정확하게 이해하려면 오리진에 대해 이해하고 있어야 한다. 웹 콘텐츠의 오리진(Origin)은 웹 콘텐츠에 액세스 하는데 사용되는 URL의 스키마(프로토콜), 호스트(도메인), 포트에 의해 정의된다. 스키마, 호스트, 포트가 모두 일치하는 경우에만 두 개체는 같은 오리진이라고 할 수 있다.

Web에서는 같은 오리진 콘텐츠 작업에만 한정되며 (동일 출처 정책)이 제약은 CORS를 사용하여 완화 할 수있다.

동일한 오리진 예

스키마(http) 및 호스트(www.devkuma.com)이 동일하므로 동일한 오리진

http://www.devkuma.com/app1/index.html
http://www.devkuma.com/app2/index.html

서버는 기본적으로 80번 포트이고, HTTP 콘텐츠를 제공하므로 동일한 오리진

http://www.devkuma.com:80
http://www.Devkuma.com

다른 오리진 예

스키마가 다르다.

http://devkuma.com/app1
https://devkuma.com/app2

호스트가 다르다. (서브 도메인이 다르다.)

http://devkuma.com
http://www.devkuma.com
http://blog.example.com

포트가 다르다.

http://www.devkuma.com
http://www.devkuma.com:8080

왜 CORS가 필요한가?

브라우저는 보안상의 이유로 동일한 오리진 정책을 채택하고 있다. 다른 출처에서 내 자원에 마음대로 접근 할 수 없도록 하기 위해 사용한다.

만약 내가 서비스하지 않는 사이트에서 세션의 요청이 얻을 수 있다면 그 사이트는 내 세션 탈취되면 해당 세션에서 나쁜 짓을 할지도 모른다. 그래서 브라우저는 이러한 요청을 막아주고 있다.

피싱 사이트가 대표적인 공격의 사례에서 이런 공격은 멈추고 내가시켰다 오리진 만 요청할 수 있도록하기 위해 필요한다.

CORS는 어떻게 작동 하는가

브라우저가 리소스를 요청할 때 추가적인 헤더에 정보를 담는다. 내 origin은 무엇이고 어떤 메소드를 사용해서 요청을 할 것이고 어떤 헤더들을 포함할 것인지를 담아서 서버에 전송한다. 서버는 서버가 응답할 수 있는 origin들을 헤더에 담아서 브라우저에게 보낸다. 브라우저가 이 헤더를 보고 해당 origin에서 요청할 수 있다면 리소스 전송을 허용하고 만약 불가능하다면 에러를 발생시킨다.

CORS preflight 요청

HTTP 헤더의 전송로 구성된 시스템이며, 브라우저가 오리진을 넘은 요구에 대한 응답에 프런트 엔드 JavaScript 코드가 접근하는 것을 차단할지 여부를 결정한다.

Request Header 목록

  • Origin

    • 어떤 오리진에서 접근하고 있는지를 보여준다.
  • Access-Control-Request-Method

    • preflight 요청을 할 때 실제 요청에서 어떤 메서드를 사용할 것인지 서버에게 알리기 위해 사용된다.
  • Access-Control-Request-Headers

    • preflight 요청을 할 때 실제 요청에서 어떤 header를 사용할 것인지 서버에게 알리기 위해 사용된다.

Response Header 목록

  • Access-Control-Allow-Origin

    • 브라우저가 해당 origin이 자원에 접근할 수 있도록 허용합니다. 혹은 *은 credentials이 없는 요청에 한해서 모든 origin에서 접근이 가능하도록 허용한다.
  • Access-Control-Expose-Headers

    • 응답의 일부로 어떤 헤더를 공개해도 좋은지를 헤더 이름을 열거하여 보여준다.
    • 브라우저가 액세스할 수있는 서버 화이트리스트 헤더를 허용한다.
  • Access-Control-Max-Age

    • preflight 요청 결과를 캐시 할 수있는 시간을 나타낸다.
  • Access-Control-Allow-Credentials

    • Credentials가 true 일 때 요청에 대한 응답이 노출될 수 있는지를 나타낸다.
    • preflight요청에 대한 응답의 일부로 사용되는 경우 실제 자격 증명을 사용하여 실제 요청을 수행 할 수 있는지를 나타낸다.
    • 간단한 GET 요청은 preflight 되지 않으므로 자격 증명이 있는 리소스를 요청하면 헤더가 리소스와 함께 반환되지 않으면 브라우저에서 응답을 무시하고 웹 콘텐츠로 반환하지 않는다.
  • Access-Control-Allow-Methods

    • preflight 요청에 대한 대한 응답으로 허용되는 메서드들을 나타냅니다.
  • Access-Control-Allow-Headers

    • preflight 요청에 대한 응답으로 사용되어 실제 요청을 할 때 사용할 수있다 HTTP 헤더를 나타낸다.요청에 대한 대한 응답으로 실제 요청 시 사용할 수 있는 HTTP 헤더를 나타냅니다.

CORS 사용 예제

여기에 하나의 Web 사이트 https://api.devkuma.com 대해 다른 Web 사이트 https://www.devkuma.com 에 대한 HTTP(S)에 대한 액세스를 허용할 경우를 예제로 설명한다.

간단하게 데이터로드를 허용할 경우

단순히 XHR 나 Fetch API에서 GET과 POST를 허용하려면 다음과 같이한다. 먼저 클라이언트 측에서 XHR의 경우는 특별한 연구가 필요없이 Fetch API의 경우 옵션에 따라 CORS를 사용하는 것을 선언한다.

클라이언트 JavaScript (XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.devkuma.com');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

클라이언트 JavaScript (Fetch)

fetch('https://api.devkuma.com', {
  mode: 'cors'
}).then(onLoadFunc);

그리고, Web 서버 쪽에서는 Origin이 달라도 액세스를 허용하도록 브라우저에 명시적으로 알리기 위해 HTTP 응답 헤더에 적절한 정보를 추가한다.

먼저 브라우저에서 서버로 전송되는 HTTP 요청 헤더는 다른 Origin 액세스인 경우에 Origin는 필드가 포함된다.

GET /api HTTP/1.1
Origin: https://www.devkuma.com

만약 Origin 내용이 신뢰할 수 있는 Web 사이트 Origin 경우 HTTP 응답 헤더에

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.devkuma.com

와 같은 내용을 추가하면 브라우저로 액세스가 허용되는 것이다. 또한, 이러한 간단한 예제에만 어떤 Web 사이트에 다른 Origin 액세스를 허용하도록 와일드 카드로 지정할 수 있다 (하위 도메인 등의 부분 지정은 할 수 없다).

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *

Cookie를 허용하려면

HTTP(S) 통신시에 Cookie의 송수신을 허용하려면 브라우저와 서버 모두에서 조금만 조작해야 한다. 먼저 브라우저의 JavaScript에서는 다음과 같이 작성한다. 아래 예제에서는 Access-Control-Allow-Origin에 와일드 카드 지정이 허용되지 않기에 주의가 필요하다.

클라이언트 JavaScript (XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.devkuma.com');
xhr.withCredentials = true;
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

클라이언트 JavaScript (Fetch)

fetch('https://api.devkuma.com', {
  mode: 'cors',
  credentials: 'include'
}).then(onLoadFunc);

이에 서버 측에서 HTTP 응답 헤더에 다음과 같은 내용을 추가한다.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.devkuma.com
Access-Control-Allow-Credentials: true

정교한 HTTP 통신을 사용하려면

CORS 사양 은 다음 조건에 하나라도 해당하는 경우 실제 HTTP 요청 (GET과 POST)를 수행하기 전에 Preflight request 로 OPTIONS 요청을 할 정해져 있다. 이 경우 서버 측에서 GET 및 POST 이외에 OPTIONS에서도 같은 CORS 대응이 필요하게 되므로 주의가 필요하다.

HTTP 요청 메소드가 GET, POST, HEAD 이외이다. HTTP 요청 헤더에 Accept, Accept-Language, Content-Language 이외의 필드가 포함되어 있거나, Content-Type 필드에 application/x-www-form-urlencoded, multipart/form-data, text/plain 이외의 내용이 지정되어 있다. Preflight request 다음과 같은 HTTP 요청 헤더가 포함되어 있다.

OPTIONS /api HTTP/1.1
Access-Control-Request-Method: {요청 HTTP 메소드 (GET, POST 등)}

이 Preflight request에 대한 응답으로 예를 들어, 적어도 다음과 같은 요령으로 Origin을 넘는 액세스로 허용하는 HTTP 요청 메소드를 지정해야 한다.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.devkuma.com
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS

요청에 자신의 HTTP 요청 헤더를 추가하려면

예를 들어, 브라우저 측에서 X-MyRequestX-MyOption라는 헤더를 추가했다고 하자.

클라이언트 JavaScript (XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.devkuma.com');
xhr.withCredentials = true;
xhr.setRequestHeader('X-MyRequest', 'this-is-cors-test');
xhr.setRequestHeader('X-MyOption', 'my-option');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

클라이언트 JavaScript (Fetch)

fetch('https://api.devkuma.com', {
  method: 'GET',
  mode: 'cors',
  credentials: 'include',
  headers: {
    'X-MyRequest': 'this-is-cors-test',
    'X-MyOption': 'my-option'
  }
}).then(onLoadFunc);

이 경우 먼저 다음과 같은 HTTP 요청 헤더를 포함하여 Preflight request가 브라우저에서 서버로 전송된다.

OPTIONS /api HTTP/1.1
Origin: https://www.devkuma.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-MyRequest,X-MyOption

서버 측에서는 이러한 요청 헤더에 표시된 메소드와 헤더를 허용할지 여부를 판단하여 응답 헤더를 반환한다. Access-Control-Allow-Methods로 지정된 메소드와 Access-Control-Allow-Headers에서 지정된 헤더가 이 후에는 브라우저에서 전송하는 HTTP 요청이 허용된다. (해당 헤더는 preflight과 실제 요청을 모두 필요하다.)

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.devkuma.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption

응답에 자신의 HTTP 응답 헤더를 추가하고 브라우저에서 읽어 내려면

다른 Origin에 액세스하는 경우, 예를 들어, 브라우저 측의 코드는 아래와 같다.

클라이언트 JavaScript (XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.devkuma.com');
xhr.withCredentials = true;
xhr.setRequestHeader('X-MyRequest', 'this-is-cors-test');
xhr.setRequestHeader('X-MyOption', 'my-option');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

function onLoadFunc() {
  var myResponse = xhr.getResponseHeader('X-MyResponse');
  var myOption = xhr.getResponseHeader('X-MyOption');
}

클라이언트 JavaScript (Fetch)

fetch('https://api.devkuma.com', {
  method: 'GET',
  mode: 'cors',
  credentials: 'include',
  headers: {
    'X-MyRequest': 'this-is-cors-test',
    'X-MyOption': 'my-option'
  }
}).then(onLoadFunc);

function onLoadFunc(response) {
  var myResponse = response.headers.get('X-MyResponse');
  var myOption = response.headers.get('X-MyOption');
}

이에 대해 서버 측에서 아래와 같이 응답한다.

HTTP/1.1 200 OK
X-MyResponse: this-is-successful-response
X-MyOptions: good-result

같은 고유 응답 헤더를 브라우저에 반환하려고 하는 경우, 브라우저는 이러한 응답 헤더의 내용을 받으려고 하면 비보안 헤더에 액세스하려고 한 것으로 간주되어 액세스가 허용되지 않도록 되어 있다. (액세스가 허용되는 응답 헤더는 Cache-Control, Content-Language, Content-Type, Expires Last-Modified, Pragma인거 같다.)

이러한 고유 응답 헤더에 액세스 브라우저 허용하려면 허용할 응답 헤더를 Access-Control-Expose-Headers로 지정한다.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.devkuma.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
Access-Control-Expose-Headers: X-MyResponse,X-MyOption

또한 당연히 Set-CookieSet-Cookie2Access-Control-Expose-Headers로 지정해도 XHR나 Fetch API으로 읽을 수 없다.

그런데 Preflight request는 매번 진행 되는가?

Preflight request는 서버 측에서 브라우저 캐시하는 유효 기간을 지정할 수 있다. 이 기간이 있으면 첫 번째 Preflight request가 이 후에는 같은 URL에 대한 HTTP 요청에도 적용이 된다.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.devkuma.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
Access-Control-Expose-Headers: X-MyResponse,X-MyOption
Access-Control-Max-Age: 864000

Access-Control-Max-Age에는 유효 기간을 초 단위로 지정한다. 위의 예에서는 10일(10(일) × 24(시간) × 60(분) × 60(초) = 864,000 (초))으로 되어 있다.

참조