Loading
2022. 8. 6. 00:15 - lazykuna

CORS 요청이 이루어지기까지

CORS는 Cross-Origin Resource Sharing 의 줄임말로, 다른 도메인에 요청을 수행하여 받아온 값에 접근하는 것을 허용하는 절차를 의미합니다.

보통 로컬 테스트를 할 때 많이들 사용하게 되는데, 왜냐하면 로컬에서 웹 client를 돌리면 [localhost](http://localhost) 도메인을 가지고 있는 반면, 웹서버는 remote 도메인을 가지고 있는 경우가 있게 되는 상황이 되기 때문입니다. 서로 도메인이 다르니, CORS를 설정하지 않으면 로컬에서 response를 받아올 수 없게 됩니다.

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

안 그래도 모종의 일로 로컬테스트를 위해 CORS 환경을 구성해야 할 일이 생겼는데, 이참에 작업한 내용을 간단하게 정리해둡니다.

0. 왜 이렇게 거추장스러운 걸 하는거야 …

이렇게 하지 않으면 어떤 도메인이든 서버에 요청을 마음껏 날릴 수 있게 되고, 중간에서 정보를 가로챌 수 있게 되기 때문입니다. 중간에서 UI만 그럴싸하게 생긴 피싱 사이트가 요청을 중간에 가로채서 spoofing 한다고 생각하면…

그래서 기본적으로 같은 도메인에서 수행되는 요청만을 허용하도록 하게 되었고, 혹시라도 제 3자의 요청을 허용하도록 하기 위해 CORS라는 절차를 만들게 된 것입니다. CORS 절차를 충족하지 못하면 해당 요청은 웹 브라우저상에서 막히게 되어 웹 클라이언트에서 절대로 결과에 접근할 수 없습니다.

따라서, CORS 처리는 전적으로 server side에서 해 주어야 합니다. 클라이언트가 해당 프로토콜을 우회할 수 있는 방법은 없습니다! 그나마 다행인 건, 요즘은 middleware나 nginx ingress와 같은 곳에서 CORS 관련 request/response header을 선처리해주는 편리한 기능들이 많이 있어서, 실제로 이러한 요청을 위한 개발을 할 필요는 거의 없습니다.

1. Preflight request

일부 요청의 경우 CORS preflight를 요청합니다. (꼭 사용해야 하는 것은 아니고, 일부 요청의 경우 필요)

Method가 OPTIONS으로 표시되는 것들이 이것들인데, GET/POST와 같은 일반적인 요청과 다릅니다! 보통 이는 CORS Middleware에서 처리해야 되는 요청입니다.

그래서 preflight request가 뭐냐면… 일종의 사전 요청입니다. 아래 예시의 경우 “너 이 메서드 DELETE CORS 지원해?” 라고 물어보는 거라고 보시면 됩니다.

OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

지원한다고 하면 아래와 같은 요청이 돌아오게 됩니다.

HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Credentials: true

그러면 이제 웹 클라이언트는 안심하고 CORS 헤더를 넣어서 다시 한 번 (진짜) 요청을 보내게 됩니다.

확인해야 할 점

  • 라우터 엔드포인트에서 “OPTIONS” 메서드를 지원하는지 반드시 확인해야 합니다. 안 그러면 middleware 박아놨다 한들 라우터에서 404/405 Response를 해버려서 CORS preflight가 실패해버립니다.
  • CORS preflight가 실패할 경우, (일반적으로) client가 실제 요청을 할 때 CORS 헤더를 빼버리고 요청하게 되어 있습니다. 자연스럽게 실제 요청도 CORS 헤더 부재로 인해 실패하게 됩니다.

2. General Http request

실제 요청의 경우에도 별반 다를 건 없고, Access-XXX 관련 CORS 헤더를 넣어서 보내게 됩니다. (preflight를 보내지 않은 경우) 필요하다면 Access-Control-Request-XXX 헤더를 넣어서 CORS 헤더를 요청할 수 있습니다.

CORS에서 핵심은 Response의 헤더입니다. Response에 Access-Control-XXX 관련 헤더가 있어야, 클라이언트에서 response에 접근할 수 있습니다. 몇 가지 정리해두면…

  • Access-Control-Allow-Origin : request의 또는 * (모든 origin 허용)이 가능합니다. 필수
  • Access-Control-Allow-Credentials : credentials (cookie와 같은 정보)를 회신받을 수 있는지를 의미합니다. credentials 정보는 거의 쓰게 되어있으니, 일반적으로 필수입니다.

일반적인 요청의 경우, 사실상 위 두개 정보가 필요합니다.

다만 preflight의 경우 추가 response header이 있습니다.

  • Access-Control-Allow-Methods : (preflight용 응답) CORS를 허용할 methods들 (보통 asterisk)
  • Access-Control-Allow-Headers : (preflight용 응답) CORS 허용할 헤더들 (이것도 걍 asterisk)

“Credential include”?

위에서 이야기했듯이, cookie와 같은 정보를 보내고 받을 때 credential 정보가 필요합니다. 그리고 이를 CORS에서 사용하기 위해서는 Access-Control-Allow-Credentials 헤더가 반드시 명시되어 있어야 합니다.

다만 이 헤더를 사용하게 되면 Access-Control-Allow-Origin 에서 asterisk(*)를 사용할 수 없게 됩니다! 보안상의 문제로 막은 것으로 보이는데, 따라서 request의 origin을 이용하여 그대로 회신하는 방법을 사용하게 됩니다.

TMI. Client Side: no-cors 옵션

CORS 요청이 충족되지 않는 경우, 웹브라우저 상에서는 아예 header만 까보고 response 수신 자체를 막아버리게 됩니다.

적어도 response를 수신하고 싶다면, mode: no-cors 를 사용해 볼 수 있습니다.

var opts = {
  headers: {
    'mode':'cors'
  }
}
fetch(url, opts)

하지만 그래도 웹 클라이언트에서는 결과를 여전히 열어볼 수 없습니다! response가 success/404도 아닌 opaque로 오는데, 이는 브라우저가 결과를 막아버렸다는 뜻입니다.

아주 한정적인 용도 아니면 별로 쓸 일이 없는 옵션입니다.