이번에 프로젝트를 진행하며 CORS
정책을 만나게 되었습니다. 개발 중 흔히 겪는 문제라고 알려져 있는 CORS
에 대해 포스팅해보고자 합니다.
에러 메시지를 보면 No 'Access-Control-Allow-Origin' header
라고 나와있는데 이 메시지를 기억해두면 좋을 것 같습니다.
CORS는 말 그대로 교차 출처 리소스 공유라고 해석할 수 있습니다. 여기서 의미하는 교차 출처란 다른 출처를 의미합니다.
여기서 출처(Origin)은 무엇을 의미하길래 다른 출처라고 하는 것인지 알아보겠습니다.
웹 콘텐츠의 위치를 나타내는 URL은 아래와 같은 구성요소를 가지고 있습니다.
이때 웹 콘텐츠의 출처(Origin)는 접근할 때 사용하는 URL의 스킴, 호스트, (그림에서 나타나 있지는 않는) 포트로 정의됩니다. 두 객체의 스킴, 호스트, 포트가 모두 일치할 경우 같은 출처를 가졌다고 말할 수 있습니다.
다른 출처에 대한 정책이 존재한다면 같은 출처에 대한 정책도 존재합니다. 바로 SOP
입니다.
SOP는 어떤 출처에서 불러온 리소스가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한
하는 규칙입니다.
하지만 웹 상에서 다른 출처의 리소스를 사용하는 것은 흔한 일이기 때문에 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 다른 출처라도 허용하기로 결정했습니다. 이 중 하나가 바로 CORS 정책을 지킨 리소스 요청입니다.
그러면 어떤 것이 같은 출처이고, 어떤 것이 다른 출처 일까요?
예를 들어, https://comic.naver.com/index
이라는 URL이 있다고 가정해보겠습니다.
이 URL과 같은 출처는 다음과 같습니다.
https://comic.naver.com
https://comic.naver.com/index/1
https://comic.naver.com/index?hello=1
그렇다면 다른 출처는 어떤 것이 있을까요?
http://comic.naver.com/
https://naver.com
https://comic.naver.com:8080
이제 어떤 방법을 통해 다른 출처의 리소스를 안전하게 사용할 수 있는지 알아보겠습니다.
브라우저는 웹 서버에 요청을 보낼 때 요청 헤더의 Origin
이라는 필드에 출처를 함께 담아 보내게 됩니다.
GET / HTTP/1.1
Host: foo.example
...
Origin : http://foo.example
...
이후 이 요청에 대한 응답으로 서버는 Access-Control-Allow-Origin
값에 이 리소스에 접근하는 것을 허용된 출처에 대해 알려주게 됩니다.
HTTP/1.1 200 OK
Date: Sun, 01 Jul 2023 15:26 KST
...
Access-Control-Allow-Origin: http://foo.example
이 응답을 받은 브라우저는 자신이 보냈던 Origin
값과 서버각 응답한 Access-Control-Allow-Origin
을 비교한 후 이 응답이 유효한지 확인하게 됩니다.
기본적인 흐름은 이렇지만 CORS 접근 제어 시나리오에는 세 가지 시나리오가 있습니다.
Preflight request일때 브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어 보내게 됩니다.
예비 요청은 OPTIONS
헤더를 사용해 다른 도메인의 리소스로 HTTP 요청을 보내 실제 요청이 안전한지 확인합니다.
위 요청에서 preflight request는 /doc
호스트에 요청을 보내고 서버는 Access-Control-Allow-Origin : https://foo.example
을 응답해주고 있습니다. foo.example
도메인을 가진 서버는 해당 리소스에 접근 가능한 출처는 오직 https://foo.example
이라는 것을 알려주는 것입니다.
클라이언트가 서버에게 보내는 요청을 살펴보면 아래와 같은 헤더가 존재합니다.
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method : POST
는 preflight request의 일부로, 실제 요청을 전달할 때 POST로 전송된다는 것을 알려줍니다.Access-Control-Request-Headers: X-PINGOTHER, Content-Type
는 실제 요청을 전달할 떄 X-PINGOTHER
와 Content-Type
사용자 정의 헤더와 함께 전송된다는 것을 서버에 알려줍니다.이를 받은 서버는 아래와 같은 헤더들을 응답에 포함시켜 전송합니다.
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
여기서 중요한 것은 Access-Control-Allow-Origin
헤더 입니다. 서버는 이 리소스에 접근 가능한 출처는 http://foo.example
이라고 알려주는 것입니다.
이렇게 preflight request가 완료되면 본 요청을 전송하게 됩니다.
단순 요청은 프리플라이트 요청과 다르게 예비요청 없이 본 요청을 바로 보내고, 서버가 이에 대한 응답으로 Access-Control-Allow-Origin
과 같은 값을 보내주면 CORS 정책 위반 여부를 확인하는 방법입니다.
프리플라이트 요청보다 단순해보이지만 아래와 같은 조건을 모두 만족해야 합니다.
application/x-www-form-urlencoded
, multipart/form-data
, text/plain
만 허용합니다.application/json
타입을 가지도록 설계되기 때문에 이를 만족시키는 상황은 나오기 어려울 것 같습니다.인증정보를 포함한 요청은 HTTP cookies
나 HTTP Authorization
정보를 인식합니다. 그렇기 때문에 다른 출처 간 통신에서 보안을 강화하고 싶을 때 사용합니다.
기본적으로 XMLHttpRequest
나 Fetch
호출에서 브라우저는 인증정보를 보내지 않습니다. 그렇기 때문에 XMLHttpRequest
객체나 Request
생성자가 호출될 때 특정 플래그를 설정해야 합니다.
fetch('http://foo.example', {
credentials: 'include'
});
credentials
정보를 include
로 설정하게 되면 브라우저가 응답을 줄 때 Access-Control-Allow-Credentials: true
헤더가 없는 응답을 거부합니다.
백엔드 개발자로서 CORS 에 대처하기 위해서는 Access-Control-Allow-Origin
헤더에 알맞은 값을 세팅해 주는 것입니다. 이때 와일드 카드 *
을 사용하면 당장은 편할 수 있겠지만 모든 출처에서 오는 요청을 받겠다는 의미이기 때문에 좋지 않습니다. 따라서 출처를 명시해주는 것이 좋습니다.
Origin
, Access-Control-Allow-Origin
HTTP 헤더등을 사용해서 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 방식이다.Access-Control-Allow-Origin
헤더에 알맞은 값을 세팅해주어야 한다.https://developer.mozilla.org/ko/docs/Glossary/Origin
https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy