프론트엔드 개발을 해 본 사람이라면 CORS 이슈를 정말 많이 접해봤을 것이다. CORS는 뭐고 왜 굳이 우리를 이렇게 귀찮게 하고 어떻게 하면 해결할 수 있을까?
CORS (Cross-Origin Resource Sharing)
CORS는 이름에서도 알 수 있다시피 서로 다른 출처에서 자원이 공유되는 것을 의미한다. 따라서 CORS에 대해 이해하기 위해서는 출처 (Origin)이라는 것이 어떻게 정의내려질 수 있는지가 중요하다. 인터넷 URL은 프로토콜, 호스트, 경로(path), 쿼리문, 프래그먼트 등 여러 가지 요소로 구성되어 있다. (URL 구조 관련 포스트 참고) 그 중에서 Protocol, Host, Port 총 세 가지가 일치하면 하나의 Origin으로 여겨진다. (일반적으로 포트 번호는 규약에 맞춰서 각 프로토콜에 해당하는 것을 사용하기 때문에 실질적으로는 Protocol, Host 두 가지가 Origin을 결정한다고 보면 된다.)
그렇다면 서로 다른 Origin 간의 자원 교류는 왜 막아놓은 것일까? 그것은 서로 다른 Origin 간에 통신이 이루어질 때 브라우저는 매우 취약해지기 때문이다. (사실 모든 보안이 그렇다. 보안적으로 가장 취약한 부분은 두 개가 연결되거나 전환되는 시점이다.) 이러한 문제를 막기 위해 동일 Origin끼리만 리소스를 주고받을 수 있게 하는 SOP(Same-Origin Policy) 정책이 등장하게 되었다.
그러나 현실에서는 다른 출처에 존재하는 리소스를 가져와서 사용하는 것은 매우 흔히 일어나는 일이다. 따라서 서로 다른 Origin 간의 교류를 무조건적으로 막는 것이 아니라 그것이 허용되는 일부 경우를 구분하는 것으로 합의가 이루어졌는데 이 중 하나가 바로 CORS 정책을 준수한 통신이다.
기본적인 CORS의 동작 원리는 다음과 같다. 우선 브라우저에서 request를 보낼 때 헤더의 Origin 필드를 통해 요청을 보내는 Origin의 정보를 보낸다. 그리고 서버는 그에 대한 response를 보낼 때 Access-Control-Allow-Origin 필드에 해당 리소스에 접근 가능한 Origin을 보내준다. 브라우저는 request의 Origin과 response의 Access-Control-Allow-Origin을 비교하여 해당 통신이 CORS 정책을 준수했는지 여부를 판단한다. 만약 Access-Controll-Allow-Origin에 *(와일드카드)를 넣어주면 모든 request에 대해 CORS 정책을 준수시킬 수 있다. (그렇지만 localhost 환경에서 개발중인 프로젝트를 테스트하는 것 정도가 아니라면 와일드카드를 사용하는 것은 보안 관점에서 위험하므로 지양하자) 참고로 CORS 준수 여부는 브라우저에서 판별하는 것이다. 따라서 클라이언트 측에서 CORS 에러가 발생하여도 서버 측에서는 그 사실을 인지하지 않는다.
구체적인 CORS 동작 시나리오는 크게 세 가지가 존재한다.
Preflight Request
Preflight Request 시나리오에서 브라우저는 request를 두 번에 나눠서 보낸다. 예비 request은 본격적인 request가 이루어지기 전에 브라우저 측에서 해당 통신이 안전한지 확인하기 위해 전송하는 것으로 request Origin에 대한 정보, 본 요청에 대한 정보(ex. 메소드 타입) 등을 포함한다. 브라우저는 예비 request에 대한 response의 header를 통해 Access-Control-Allow-Origin 필드를 확인함으로써 본 request를 보낼지 여부를 결정한다. 대부분의 경우에는 Preflight Request를 따라서 두 번에 걸친 request-response가 이루어진다.
Simple Request
Preflight Request와 다르게 예비 요청 없이 한 번의 request만을 보내는 방식이다. 물론 Simple Request는 아무 때나 사용할 수 없으며 다음 세 가지 조건을 충족한 아키텍쳐에서만 사용이 가능하다. (특히 2번과 3번 조건 충족이 어렵다고 한다. 사용자 인증을 위해 자주 사용되는 Authorization 헤더 혹은 API에서 자주 사용되는 application/json, text/xml 등의 Content-Type이 모두 허용되지 않기 때문이다.)
(1) request에서 Get, Head, Post 세 가지 메소드만을 사용한다.
(2) Accept, Accpet-Language, Content-Language, Content-Type, DPR, Downlink, Save-data, Viewport-Width, Width를 제외한 헤더는 사용하지 않는다.
(3) Content-Type은 application/x-www-form-urlencoded, multipart/form-data, text/plain만을 사용한다.
Credentialed Request
기본적으로 브라우저는 동일한 Origin이 아니라면 쿠키 정보, 인증 관련 헤더 정보 등을 request에 포함시키지 않는다. 하지만 credentials 옵션을 수정하면 인증 관련 정보 포함 여부를 설정할 수 있다. 적용할 수 있는 값은 세 가지가 있다. 기본값인 same-origin에서는 동일 출처일 때에만 인증 정보를 포함하며 include는 모든 request에 인증 정보 포함, omit은 모든 request에 정보를 포함시키지 않는다. 따라서 웹 애플리케이션에서 다른 Origin의 서버에 request를 보내며 쿠키를 포함한 인증 정보를 전송해야 한다면 credentials 옵션을 'include'로 바꿔줘야 한다. 만약 include를 선택하는 경우 Access-Control-Allow-Origin의 값에 더 이상 *(와일드카드)를 사용할 수 없으며 응답 헤더의 Access-Control-Allow-Credentials는 true로 값이 설정되어야 한다. 만약 javascript의 axios를 활용하는 경우 withCredentials 옵션에 true를 넣어줌으로써 간단하게 credentials 옵션에 include를 적용시킬 수 있다.
CORS 이슈 해결법
Access-Control-Allow-Origin 설정
CORS 정책을 준수하는 가장 대표적인 방법은 서버 측에서 헤더의 Access-Controll-Allow-Origin을 수정하는 방법이다. 대부분의 백엔드 프레임워크에서는 CORS 관련 설정을 위해 미들웨어 라이브러리를 제공하기 때문에 직접 헤더를 설정하는 것보다 편리하게 허용 Origin을 명시해줄 수 있다.
프록시 서버 사용하기
로컬 개발 환경에서는 webpack-dev-server을 활용하여 프록싱 기능을 이용하면 매우 편리하게 CORS 정책을 우회할 수 있다. 예를 들어서 실제로는 API 서버로 향하는 URL에 대해 /api로 프록시를 설정해주면 webpack의 프록싱을 통해 실제로는 다른 Origin으로 request를 전송하지만 브라우저 측에서는 동일한 Origin에서 request가 이루어지는 것처럼 속일 수 있다. 그러나 webpack의 프록싱을 이용하는 것은 로컬 환경에서 개발할 때에만 이용할 수 있다. (빌드한 후 배포하면 더 이상 webpack-dev-server이 구동되는 환경이 아니기 때문이다.)
이번에 유튜브 클론 프로젝트를 배포하면서 관련 문제를 확인했는데 이 경우 크게 두 가지 해결책이 있다. 첫 번째는 클라이언트와 서버 두 소스가 동일한 Origin에서 제공되도록 하는 것이다. (관련 스택오버플로우 질답 예시) 두 번째는 Netlify 기준으로 가능한 방법인데 redirects 기능을 활용하는 것이다. redirects는 간단하게 생각해서 프록싱과 비슷하다고 보면 된다. 브라우저 측에서는 A로 request를 날린다고 생각하지만 실제로는 B에 request를 날리는 기능이다. redirects는 _redirect 파일 혹은 netlify와 관련된 여러 설정을 한 번에 처리할 수 있는 netlify.toml 파일을 통해 설정해줄 수 있다. 다음은 netlify.toml 파일을 통해 redirects 기능을 이용한 예시이다.참고로 netlify.toml 파일은 루트 디렉토리에 위치해야 한다. 여기서 루트 디렉토리는 base directory 기준을 의미하는 것이 아니다. 예를 들어서 client와 server가 sub directory로 있는 git repository가 있을 때 netlify로 클라이언트를 배포하고 싶다면 base directory는 client/겠지만 netlify.toml 파일은 그냥 루트 폴더에 위치해야 한다. (참고로 다른 Vercel과 같은 다른 호스팅 서비스는 아직 이용해보지 않아서 잘 모르겠다. 아마 해당 서비스들에도 관련된 기능이 있지 않을까 싶다.)
'프론트엔드 기본개념 복습' 카테고리의 다른 글
[Network] HTTP Request Method와 Restful API (0) | 2022.02.25 |
---|---|
2021-09-28 UI & UX (0) | 2021.09.28 |
2021-10-04 브라우저 저장소와 쿠키 (0) | 2021.09.19 |