1. CORS
CORS는 Cross Origin Resource Sharing의 줄임말이다.
mdn 에서 우리말로 번역했을 땐 '교차 출처 리소스 공유'라는 뜻이 되는데 뭔가 이해하기 어렵다,,
그래서 교차 출처 보다는 다른 출처, 즉 '다른 출처 간 리소스를 공유하는 것'이라고 이해하면 좋을 것 같다.
그리고 출처라는 말이 나오는데 출처란 Protocol과 Host, 포트 번호까지 모두 합친 것을 의미한다.
https://www.naver.com 까지가 출처가 되겠다.
이 출처를 이루는 세 가지 중 하나라도 다르면 다른 출처라고 생각하면 된다.
브라우저의 개발자 도구에서 Location의 origin 프로퍼티에 접근함으로써 손 쉽게 애플리케이션이 실행되고 있는 출처를 알아낼 수 있다.
2. 같은 출처와 다른 출처의 구분
위에서 같은 출처란 프로토콜, 호스트, 포트 번호까지 모두 같으면 같은 출처이고 그중 하나라도 다르면 다른 출처로 인식한다고 했다.
그럼 그러한 출처를 비교하는 로직이 어디에서 이루어지냐 하면 브라우저에 구현되어 있는 스펙이다.
만약 우리가 CORS 정책을 위반하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답을 하고, 이후 브라우저가 이 응답을 분석해서 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버리는 순서인 것이다.
3. CORS 동작 원리
그럼 본격적으로 어떤 방법을 통해 서로 다른 출처를 가진 리소스를 안전하게 사용할 수 있는지 알아보도록 하자.
기본적으로 웹 클라이언트가 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin이라는 필드에 현재 요청을 보내는 출처를 함께 담아 보낸다.
이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-Allow-Origin이라는 값에 “허용된 출처”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin의 값을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.
기본적인 흐름은 이렇게 간단하지만, 사실 CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경되기 때문에 여러분의 요청이 어떤 시나리오에 해당되는지 잘 파악해야 CORS 정책 위반으로 인한 에러를 디버깅하는 것이 한결 쉬울 것이다.
3 - 1) Preflight Request
프리플라이트(Preflight) 방식은 일반적으로 우리가 개발할 때 가장 많이 마주치는 시나리오이다.
이 시나리오에 해당하는 상황일 때 브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.
이때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 부르는 것이며, 이 예비 요청의 HTTP 메서드는
OPTIONS 메소드가 사용된다.
예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.
우리가 fetch API를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내리면 브라우저는 서버에게 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 현재 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내주게 된다.
이후 브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책을 비교한 후, 이 요청을 보내는 것이 안전하다고 판단되면 같은 엔드포인트로 다시 본 요청을 보내게 된다. 이후 서버가 이 본 요청에 대한 응답을 하면 브라우저는 최종적으로 이 응답 데이터를 자바스크립트에게 넘겨준다.
3 - 2) Simple Request
단순 요청은 예비 요청을 보내지 않고 바로 서버에게 본 요청을 한 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin과 같은 값을 보내주면 그때 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다. 즉, 프리플라이트와 전반적인 로직 자체는 같되, 예비 요청의 존재 유무만 다르다.
하지만 아무 때나 단순 요청을 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있다. 게다가 이 조건이 조금 까다롭기 때문에 일반적인 방법으로 웹 애플리케이션 아키텍처를 설계하게 되면 거의 충족시키기 어려운 조건들이라 이런 경우는 드물다. 조건은 아래와 같다.
- 요청의 메서드는GET, HEAD, POST 중 하나여야 한다.
- Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안 된다.
- 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
사실 1번 조건의 경우는 그냥 PUT이나 DELETE 같은 메서드를 사용하지 않으면 되는 것뿐이니 그렇게 보기 드문 상황은 아니다.
하지만 2번이나 3번 조건 같은 경우는 조금 까다롭다.
애초에 저 조건에 명시된 헤더들은 진짜 기본적인 헤더들이기 때문에, 이 헤더들 외에 추가적인 헤더를 사용하지 않는 경우는 드물다.
당장 사용자 인증에 사용되는 Authorization 헤더 조차 저 조건에는 포함되지 않는다.
게다가 대부분의 API는 application/json 컨텐츠 타입을 가지도록 설계되기 때문에 사실 상 이 조건들을 모두 만족시키는 상황을 만들기는 그렇게 쉽지 않은 것이 현실이다.
3 - 3) Credentialed Request
3번째 시나리오는 인증된 요청을 사용하는 방법이다.
기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다.
이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다.
이 옵션에는 총 3가지의 값을 사용할 수 있으며, 각 값들이 가지는 의미는 다음과 같다.
same-origin (기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다 |
include | 모든 요청에 인증 정보를 담을 수 있다 |
omit | 모든 요청에 인증 정보를 담지 않는다 |
만약 same-origin이나 include와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin만 확인하는 것이 아니라 검사 조건을 추가하게 된다.
만약 인증 모드가 include일 경우, 브라우저는 모든 요청을 허용한다는 의미의 *를 Access-Control-Allow-Origin 헤더에 사용하면 안 된다고 이야기한다.
이처럼 요청에 인증 정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면 브라우저는 CORS 정책 위반 여부를 검사하는 룰에 다음 두 가지를 추가하게 된다.
- Access-Control-Allow-Origin에는 *를 사용할 수 없으며, 명시적인 URL 이어야 한다.
- 응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야 한다.
4. 끝으로
CORS 정책 위반으로 인해 생기는 문제를 해결할 때 가장 번거로운 점은 문제를 겪는 사람과 문제를 해결해야 하는 사람이 다르다는 것이다.
CORS 정책은 브라우저의 구현 스펙이기 때문에 정책 위반으로 인해 문제를 겪는 사람은 대부분 프런트엔드 개발자이지만, 정작 문제를 해결하기 위해서는 백엔드 개발자가 서버 애플리케이션의 응답 헤더에 올바른 Acccess-Control-Allow-Origin이 줄 수 있도록 세팅해줘야 하기 때문이다.
'CodeStates' 카테고리의 다른 글
IM 32일차 (Postman 사용법) (0) | 2021.02.12 |
---|---|
IM 31일차 (MIME type 과 HTTP headers 의 Content-Type) (0) | 2021.02.11 |
IM 25일차 (http) (0) | 2021.02.06 |
IM 24일차 (Basic Web Architecture, Ajax) (0) | 2021.02.05 |
IM 22일차 (Promise & Async/Await) (0) | 2021.02.02 |