로컬에서 프로젝트를 진행하다보면 다음과 같은 에러 메시지를 발견할 수 있다.
Access to fetch at ‘https://testsample.test/test’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
한국말로 나름의 해석을 해보면 다음과 같다.
origin이 http://localhost:3000 인 경로(?)에서 https://testsample.test/test 로의 fetch API 접근이 차단되었다. (CORS 정책에 의해)
우리가 fetch로 보냈던 'Access-Control-Allow-Origin' 헤더 영역이 존재하지 않다라는 알 수 없는 말을 전하면서 말이다.
CORS는 교차 출처 자원 공유에 대한 약자로 여기서 출처를 나타내는 'Origin'은 바로 다음과 같다.
Origin = Protocol(Scheme) + Host + (Port)
https:// google.com (443)
생각을 해보면,
출처가 다른 어플리케이션이 서로 자원을 마음대로 이용하는 환경은 너무 위험하다(CSRF, XSS)
그래서 등장한 정책이 SOP인데
같은 출처에서만 리소스를 공유할 수 있다.
그러나 웹에서 다른 출처에 있는 리소스를 가져와서 사용하는 일은 매우 흔해서,
무작정 막는 것도 어려워 몇가지 예외 조항을 두고
예외조건을 만족하면 출처가 다르더라도 리소스 요청을 허용하기로 했는데
그 중 하나의 예외 조건이 바로 CORS인 것이다.
쉽게 설명하면, 다른 출처(origin)로 리소스를 요청 => SOP 위반
이에 더해 CORS까지 위반하게 된다면 => 리소스 사용 불가
위에서 같은 출처를 판단하는 기준은
라고 언급한 바 있다.
예를 들어 https://example.github.io:80 처럼 포트 번호가 명시되어 있다면
포트 번호도 판단하고 없다면 브라우저의 독자적 출처 비교 로직에 따라 판단한다.
( IE는 포트번호를 완전 무시한다. IE 시렁)
여기서 핵심은 출처 비교를 서버가 아닌 브라우저가 한다는 것이다.
그렇기 때문에 서버쪽 로그에는 정상적으로 응답을 했다는 로그가 남아도
브라우저에서는 오류가 발생한 것처럼 보일 수 있고 그렇기에 error tracing이 어렵다.
리소스를 요청할 때 기본적으로 HTTP 프로토콜을 사용해서 요청, 헤더의 Origin이라는 필드에
출처를 담아서 보낸다.
이후 서버가 이 요청에 대해 응답을 할 때 'Access-Control-Allow-Origin'이라는 값에
'리소스 접근을 허락받은 출처' 즉 이 Origin은 내가 자원 사용 허락해줄게! 라는 값을 전달해준다.
브라우저는 보냈던 Origin 값과 받은 'Access-Control-Allow-Origin'의 필드 값을 비교해서 여부를 판단한다.
어떻게 보면 간단해 보일 수 있지만 여기서 또 3가지의 시나리오가 있다고 하는데 하..
본 요청을 보내기 전 요청이 안전한지 확인하는 절차가 추가된 시나리오
우리가 웹 어플리케이션을 개발할 때 가장 많이 마주치는 시나리오이다.
이 시나리오에서 브라우저는 요청을 보내기 전 '예비 요청'을 서버에 전송한다.
좀 어지러운 것이,
예비 요청이 성공했어도 CORS 정책 위반으로 인한 에러가 발생할 수 있다.
(판단 시점이 예비요청 이후임)
결국 CORS를 판단의 가장 중요한 것은 'Access-Control-Allow-Origin' 값의 유효 여부이다.
(예비 요청을 실패했더라도 유효하면 위반이 아님)
가장 흔한 시나리오이기도 하지만 모든 요청에 상황에서 예비요청을 추가로 보내는 'Preflight' 하는 건 아니다.
위에서 설명한 Preflight 과정에서 예비 요청의 과정만 생략한 것이다.
특정 조건을 만족했을 경우만 가능하다. 특정 조건이란
여기서 2, 3번째 조건이 만족하기가 상당히 까다롭다.
헤더 필드값의 제한도 이미 까다롭지만
최근 대부분의 HTTP API는 text/xml, application/json 을 사용하므로 위 세 조건을 모두 만족하는 요청은 사실상 쉽지 않다.
인증된 요청을 사용하는 방법
CORS의 기본적인 방식이라기 보다는 다른 출처간 통신에서 보다
보안을 강화하고 싶을 때 사용한다.
우리가 주로 사용하는 비동기 리소스 요청 API인 XMLHttpRequest, Fetch는 별도의 옵션없이 쿠키나 인증에 관련한 정보를 헤더에 담아 요청하지 않는다.
여기서 말한 별도의 옵션이 바로 Credentials 옵션이고 3가지가 있다.
Same-Origin
Include
Omit
우리가 주로 사용하는 구글 크롬 브라우저의 Credentials 기본 값은
same-origin으로 로컬에서 보내는 요청에는 쿠키 등의 인증 정보가 담기지 않는다.
그래서 'Access-Control-Allow-Origin' 필드에 와일드카드(*)를 사용해도
안전하다고 판단하는 것이다.
근데 Include로 바꾸면 와일드카드 사용 못한다.
명시적 URL을 포함해야 하고, 응답 헤더에 Access-Control-Allow-Credentials:true를 포함해야 한다.
서버에서 요청을 보낼 때 Access-Control-Allow-Origin 필드의 값을 제대로 보내주는 것이
CORS의 가장 정석적인 해결 방법이라고 한다.
이때 와일드카드(*)를 사용해 모든 출처에 대한 요청을 허용해놓으면 당장은 편할 수 있지만
보안적으로 상당히 위험할 수 있기에 삼가는 것이 좋다.
Spring, Express, Django 등 이름 있는 백엔드 프레임워크에는 CORS 설정 관련 라이브러리가 존재한다고 하니 어렵지 않게 세팅할 수 있다고 한다.
이 부분에 대해 아직 학습이 완전하지 않아서 추후에 추가하도록 하겠다.
아마도 package.json에서 proxy 값을 임의로 설정해주는 것과 비슷한 접근인 것 같은데
단순히 우회하는 것이고 근본적인 해결 방법이 아니니 이것도 삼가도록 하자 ~!
CORS 오류를 마주해서 머리가 어질어질한 경우는 대부분 프론트엔드 개발자(잇츠 미)일 것 같은데
해결 방법은 또 서버에 있다하니 더더욱 어지러운 상황이 아닐 수 없다.
당장의 테스트를 위해 우회할 수는 있지만 근본적으로는 백엔드 개발자의 도움이 필요할 수 밖에 없다.
CORS로 어지러웠던 경험으로 나중에 한 번 꼭 포스팅하면서라도 정리해야지 라고 다짐했는데
이제라도 하게 돼서 뿌듯하다.
이 글의 99.9%는 Evan Moon님의 블로그 글 CORS는 왜이렇게 우리를 힘들게 하는걸까?에서 학습한 후
내 스스로 받아들이는 과정(단어의 사소한 변화.. ?)를 거친 것이므로 더 궁금한 것이 있다면 위 링크를 접속해서 알아보도록 하자.