야심차게 기획했던 첫 토이 프로젝트를 진행하는데 API를 이용해 데이터를 불러오려던 중 빨간색 에러가 슈슈슉떠서 몹시 당황했던 기억이 있었다. 백 없이 프론트만으로 진행중이던 프로젝트라 해결하는데에 애를 많이 먹었었다.
브라우저는 보안상의 이유로 동일 출처 정책(SOP)을 따르며 다른 출처간의 http 요청을 제한하는데 여기서 다른 출처 간의 HTTP 요청을 제한하는 것을 CORS라고 한다.
Host, Protocol, Port가 하나라도 다를 시 출처가 다른 교차출처라고 판단해서 HTTP 요청을 제한해버립니다. 만약 출처가 동일하다면 CORS에 적용되지 않고 자유롭게 요청을 보낼 수 있다.
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)
추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다. -mdn
출처(Origin)는 URL이다.
서버의 위치를 의미하는 https://google.com과 같은 URL들은 마치 하나의 문자열 같아 보여도, 사실은 여러 개의 구성 요소로 이루어져있다.
위의 구성요소 중에서 Protocol + Host(도메인) + Port(번호) 3가지가 같으면 동일 출처(Origin)라고 한다.
https://google.com 은 Protocol, Host로 이루어져있다. Port는 생략이 가능하다.
domain-a.com에서 domain-a.com으로 요청을 보내는건 호스트, 프로토콜가 같이 때문에 허용이 되지만, (포트번호는 생략됨)
domain-b.com에서 domain-a.com으로 보내는건 Host가 다르기 때문에 차단된다.
도메인 이외에, 같은 프로젝트 내에 정의된 css 파일 요청은 동일 출처 요청이고,
font같은 경우에는 다른 외부 사이트에서 실시간으로 import를 통해 가져온다면 다른 출처 요청이다.
이처럼, 같은 출처가 아닌 외부에 자원을 요청하는 경우가 있는지 잘 확인해보아야 한다.
cors 접근 제어 시나리오 (교차 출처 리소스 공유가 동작하는 방식)에는 크게 세가지가 있다.
단순 요청(Simple Request)은 공식 용어가 아닌, MDN에서 사용한 용어를 가져온 것이다. 그런데 이름이 의미하는 것과 달리, 단순 요청은 흔한 유형의 요청이 아니다. 단순 요청이 되기 위한 조건이 매우 까다롭기 때문이다. 그 조건은 대략 다음과 같다.
위와 같은 조건을 만족하는 단순 요청은 안전한 요청으로 취급되어, (뒤에서 설명할) 프리플라이트 요청이 필요 없이 단 한 번의 요청만을 전송한다.
즉, 위에서 언급한 CORS의 기본적인 동작 원리를 그대로 따른다.
다른 Origin으로 요청을 보낼 때 Origin 헤더에 자신의 Origin을 설정하고, 서버로부터 응답을 받으면 응답의 Access-Control-Allow-Origin
헤더에 설정된 Origin의 목록에 요청의 Origin 헤더 값이 포함되는지 검사하는 것이다. 이를 그림으로 나타내면 다음과 같다.
위에서 소개한 단순 요청의 조건에 벗어나는(= 안전하지 않은) 요청의 경우, 서버에 실제 요청을 보내기 전에 예비 요청에 해당하는 프리플라이트 요청(Preflight Request)을 먼저 보내서 실제 요청이 전송하기에 안전한지 확인한다. 만약 안전한 요청이라고 확인이 된다면, 그때서야 실제 요청을 서버에게 보낸다. 따라서 총 두 번의 요청을 전송한다.
프리플라이트 요청의 특징은 다음과 같다.
- 메소드로 OPTIONS를 사용한다.
- Origin 헤더에 자신의 Origin을 설정한다.
- Access-Control-Request-Method 헤더에 실제 요청에 사용할 메소드를 설정한다.
- Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정한다.
서버는 이러한 프리플라이트 요청에 대해 다음과 같은 특징을 가진 응답을 제공해야 한다.
- Access-Control-Allow-Origin 헤더에 허용되는 Origin들의 목록 혹은 와일드카드(*)를 설정한다.
- Access-Control-Allow-Methods 헤더에 허용되는 메소드들의 목록 혹은 와일드카드(*)를 설정한다.
- Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록 혹은 와일드카드(*)를 설정한다.
- Access-Control-Max-Age 헤더에 해당 프리플라이트 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정한다.
이러한 응답을 받고 나면 브라우저는 이 응답의 정보를 자신이 전송한 요청의 정보와 비교하여 실제 요청의 안전성을 검사한다. 만약 이 안전성 검사에 통과하게 된다면, 그때서야 실제 요청을 서버에게 보낸다. 단, 이때는 Access-Control-Request-XXX
형태의 헤더는 보내지 않는다.
예를 들어, Content-Type
헤더의 값이 application/json
이고 사용자 정의 헤더로 Custom-Header를 사용하는 POST 요청
을 서버에게 보내려 한다고 해보자. 그러면 이는 단순 요청의 조건에 벗어나기 때문에 프리플라이트 요청이 필요하다. 따라서 총 두 번의 요청이 서버에게 전송되며, 이를 그림으로 나타내면 다음과 같다.
프리플라이트 요청은 API 요청 횟수에 포함되지 않는다.
위에서 소개한 두 유형의 요청은 인증 정보가 없는 경우였다.
여기서 말하는 인증 정보(Credential)란 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등을 일컫는다.
브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 기본적으로 요청에 담지 않으므로, credentials 옵션을 변경하지 않고서는 cookie를 주고 받을 수 없다.
1. 서버와 소통이 가능하다면
서버에 요청을 하자
서버에서 Access-Control-Allow-Origin 헤더에 유효한 값을 포함하여 응답을 브라우저로 보내면 해결이 가능하다.
2. 서버와 소통이 불가능하다면
프록시 서버를 사용하자
프록시 서버란? 클라이언트와 서버 간에 중계 역할을 수행하여 요청과 응답을 전달해주는 서버.
내가 설정한 출처로 변신해 해당 엔드포인트에 대한 요청을 대신 해준다.
- package.json에서 proxy 설정하기 (통신해야 할 엔드 포인트가 1개일 경우에 해당)
{
//...
"proxy": "http://localhost:4000"
}
- http-proxy-middleware 사용하기 (엔드포인트가 여러 개일 경우 해당)
npm install --save-dev http-proxy-middleware
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
createProxyMiddleware('/api', {
target: 'http://www.example.org',
changeOrigin: true
})
);
};
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EC%A0%91%EA%B7%BC_%EC%A0%9C%EC%96%B4_%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4_%EC%98%88%EC%A0%9C
https://evan-moon.github.io/2020/05/21/about-cors/#cors%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EB%B0%A9%EB%B2%95
https://escapefromcoding.tistory.com/724#3.-%EC%8B%A0%EC%9A%A9-%EC%9A%94%EC%B2%ADcredentialed-request
https://it-eldorado.tistory.com/163
https://ingg.dev/cors/#whatiscors