웹개발을 하다보면 한번쯤 마주치는 상황 중 하나는 CORS 에러다. 최근에 회사에서 백엔드 아키텍처를 MSA로 전환하고 있는데, 이에 따라 한 도메인에서 사용하던 API 중 상당수가 각자의 도메인으로 분리되었다. 별도의 API Gateway가 없는 상황에서 새로운 도메인의 API를 처음 연동하다 보니, 콘솔창에 빨간 글씨로 이렇게 뜨는 에러를 보게 되었다.
어디에서 보낸 요청이 CORS로 막히는가?
분명 Postman 등 브라우저가 이외 환경에서는 멀쩡하게 실행되던 API인데, 유독 웹에서 내가 작업중인 프론트엔드의 화면에 연동을 하려고 보니 CORS 정책에 위반되었다는 에러 로그가 뜨며 HTTP 요청에 실패하는 것이었다. 즉, 웹사이트에서 AJAX 요청을 보낼 때에만 안 된다. 즉 웹사이트를 열어주는 크롬, 사파리와 같은 브라우저에서 일어나는 문제라는 것이다. 예를 들면 브라우저가 내 로컬 환경인 localhost:8080 에서 API 서버인 localhost:5000 으로 보내는 것을 허용하지 않고 있다. 왜 어디서는 되고 어디서는 안 되는 것일까?
CORS가 무엇이고 왜 있는가?
왜 어떤 상황에서는 CORS 정책에 의해 요청이 차단되는 것일까? CORS가 왜 있고, 무슨 역할을 하는가?
https://domain-a.com이라는 커뮤니티 사이트가 있다고 가정해보자. 유저들은 이 사이트에 회원가입을 하고, 로그인을 통해 접속할 수 있다. 보통 이 사이트에 로그인을 하면, 다음에 접속했을 때 다시 아이디와 비밀번호 등의 정보를 입력할 필요가 없도록 로그인이 유지되고 있는 경우가 많다. 이는 내가 사용중인 브라우저에 토큰 등의 정보가 쿠키 등으로 저장이 되어서, 이 쿠키를 로그인했던 사이트에 접속할 때 요청에 실어보내서 로그인을 유지하는 방식이다. 그런데 이 인증정보가 들어있는 쿠키를 가지고 다른 웹사이트의 서버에 요청을 보낼 수 있도록 제한없이 허용해 줄 경우 악의적인 웹사이트가 중요한 정보를 가진 쿠키를 탈취할 위험이 있다. 따라서 브라우저는 어떤 사이트에서는 다른 사이트로 HTTP 요청을 보낼 수 없도록 SOP란 정책을 만들게 되었고 한다.
SOP? CORS?
SOP는 Same-Origin-Policy의 약자로, 동일 출처 정책이다. 말 그대로 동일한 출처인 URL끼리만 API를 통해 데이터에 접근할 수 있도록 허용하는 정책이다. 위에서 언급한 보안적 위험 때문에 만들어진 정책이고, CORS는 사실 동일하지 않은 출처 간의 요청을 제한적으로 허용할 수 있게 해주는 정책이라고 한다. CORS, 즉 Cross-Origin Resource Sharing은 다른 출처 간에 리소스를 공유할 수 있도록 하는 것을 말한다. 예를 들어 내가 프론트엔드 개발을 하고 있는 웹사이트 주소가 https://domain-a.com이고 사용하려는 API의 도메인 주소가 https://domain-b.com 이라면, 이 서로 다른 출처 간의 HTTP 요청과 응답이 가능하도록 CORS를 통해 설정해줄 수 있다는 것이다. 서로 다른 출처끼리 요청을 주고받는 행위는 SOP에 의해, 안 되는 것이 기본값이었다고 한다. 그러나 웹 생태계가 다양해지면서 여러 서비스들간에 자유롭게 데이터가 주고받아질 필요가 생겼고, 따라서 이를 합의된 출처들 간에 합법적으로 허용해주기 위해 CORS, 즉 교차 출처 자원 공유 방식이 생기게 되었다.
그럼 어떻게 CORS 조건을 설정할까?
요청을 받는 백엔드에서 이걸 허락할 다른 출처들을 미리 명시해주면 된다. 백엔드 프레임워크인 스프링이나 장고, Express 등의 문서를 살펴보면 CORS 옵션을 넣는 방법들이 명시되어 있다. 여기에다 내가 프론트엔드를 브라우저에 띄울 도메인 주소들을 넣어주면, 거기서 지정한 사이트에서는 해당 서버로 얼마든지 요청을 보낼 수 있게 된다. 여기에 아무나 보내도 되는 요청일 경우 별표(*), 와일드카드를 넣으면 어디에서나 쓸 수 있게 되겠지만....보안상 당연히 좋지 않을 것이다.
Cross Origin 요청이 보내지는 과정
https://domain-a.com에서 https://domain-b.com으로 요청을 보낼 때, 브라우저는 요청에 Origin 이라는 header를 추가한다. 이 헤더에는 요청하는 쪽의 scheme과 도메인, 포트 등의 정보가 담긴다. 이 요청을 받은 API 서버는 답장의 헤더에 지정된 Access-Control-Allow-Origin 정보를 실어서 보낸다. 여기에 https://domain-a.com이 등록된 상태라면 이 URL도 들어있게 된다. 이 경우 실행될 브라우저인 크롬은 이 둘을 비교해서 일치할 경우에만 안전한 데이터로 간주하고 응답 데이터를 받아오게 되는 것이다. 없을 경우 위에서 보여진 에러 로그와 함께, 요청에 실패하게 된다.
토큰 등 사용자 식별 정보가 담긴 요청에 대해서는 이보다 더 엄격하다. 보내는 측에서는 요청의 옵션에 credentials 항목을 true로 설정해야 하고, 받는 측에서도 아무 출처나 다 된다는 와일드카드가 아니라 보내는 쪽의 출처를 정확히 명시한 다음, Access-Control-Allow-Credentials 항목을 true로 맞춰줘야 한다. 브라우저에 저장된 쿠키가 좋지 못한 목적으로 쓰일 수 있기 때문이다.
결론
서버에서 Cross Origin 요청을 허용하는 헤더 정보에 실행하고자 하는 프론트엔드 URL인 localhost:8080 을 추가하여 해결하였다. Access-Control-Allow-Origin 헤더를 사용할 수 있으며, 쿠키를 사용해야 하는 특수한 상황에서는 Access-Control-Allow-Credentials 헤더를 사용하여 교차 출처 요청을 허용할 수 있다.
Access-Control-Allow-Origin: http://localhost:8080