회사에서 작업을 하는데 API 요청할 때 부쩍 CORS가 났다. 정상적으로 요청이 들어가다가 CORS가 터지는 보니 초기 설정문제는 아닌 것 같았고, 원인은 사실 AWS의 IAM 계정 설정 문제로 서버 자체가 죽는게 원인이었다. 회사 문제가 잘 해결된 기념으로 CORS의 개념을 다시 짚고 가고싶어서 공부하고 정리한 글.
웹에서는 출처를 공유하는 것과 관련해 두 가지 정책이 존재한다. CORS(Cross origin resource)
는 교차 출처 공유 정책과 SOP(Same Origin Policy)
동일 출처 정책이 있다.
여기서 출처(origin)
이란 URL 구조에서 프로토콜
+호스트주소
+포트번호
를 의미한다. 즉, 동일 출처란 프로토콜, 호스트주소, 포트번호가 같은 것을 말한다.
SOP란 다른 출처의 리소스를 사용하는 것을 제한하는 정책이다. XMLHttpRequest와 FetchAPI는 동일 출처 정책을 따른다. 즉, 이 API를 사용하는 웹 어플리케이션은 자신과 동일한 출처의 리소스만 불러올 수 있고, 다른 출처의 리소스를 불러오기 위해서는 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.
서로 다른 출처의 어플리케이션이 서로 통신하는 것에 대해 아무런 제약이 존재하지 않으면 CSRF(Cross Site Request Forgery)나 XSS(Cross Site Scriping) 공격에 노출될 수 있다. 예를들면, 사용자가 웹사이트에 로그인하여 정상적인 쿠키를 발급 받은 후, 악성코드가 삽입된 링크를 열었을 때, 대상 웹사이트는 믿을 수 있는 사용자가 실행한 것으로 판단한다.
교차출처 자원공유. 한 출처에서 실행중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 것을 말한다.
사진출처 - 4 Ways to Reduce CORS Preflight Time in Web Apps
브라우저는 요청을 한 번에 보내지 않고 preflight 요청을 보낸 후 실제 요청을 보낸다. preflight request는 option
메서드를 통해 다른 도메인의 리소스에 요청이 가능한지 확인작업을 한다.
preflight의 요청에는 아래와 같이 Origin, Access-Control-Request-Method, Access-Control-Request-Header가 포함되어 있어야하고, 응답은 Code 200, body가 빈 상태이다.
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
preflight 요청이 필요한 이유는 CORS가 서버가 아닌 브라우저에 구현되어 있는 스펙이기 때문이다. CORS 정책을 위반하는 리소스 요청을 하더라도 서버는 정상적으로 응답을 하고, 이후 브라우저가 응답을 분석해 CORS 정책 위반이라고 판단하면, 그 응답을 버리는 순서다. 정책에 위반된 요청이 와도 서버는 판단을 하기 못하기 때문에 이를 방지하기 위해 preflight 요청을 보내 CORS 에러가 나는지를 미리 확인한다.
아래의 조건을 모두 충족했을 때, Preflight 요청 없이 바로 요청을 보낸다
1. 메서드 : GET
HEAD
POST
2. Content Type : application/x-www-form-urlencoded
multipart/form-data
text/plain
3. Header : Accept
Accept-Language
Content-Language
Content-Type
인증정보를 포함한 요청. credentialed reqeusts는 쿠키
와 HTTP Authentication
정보를 인식한다. 기본적으로 cross site XMLHttpRequest나 Fetch 호출에서 브라우저는 자격증명을 보내지 않기때문에 XMLHttpReqeust 객체나 Reqeust 생성자가 호출될 때 특정 플래그를 설정해야 한다.
withCredentiales
를 true
access-control-allow-credentials : true
,*
를 사용해 이 헤더를 세팅하면 모든 출처에서 오는 요청을 받기 때문에 보안상 좋지 않기 때문에 출처를 정확히 명시해주는 것이 좋다.
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
export default defineConfig({
server: {
proxy: {
// string shorthand
'/foo': 'http://localhost:4567',
// with options
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// with RegEx
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
},
// Using the proxy instance
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
configure: (proxy, options) => {
// proxy will be an instance of 'http-proxy'
}
},
// Proxying websockets or socket.io
'/socket.io': {
target: 'ws://localhost:3000',
ws: true
}
}
}
})