1. CORS에 대한 기본적인 내용
- CORS 관련 이슈는 모두 COS 정책을 위반했기 때문에 발생.
- 사실 CORS라는 방어막이 존재하기 때문에 이곳 저곳에서 가져오는 리소스가 안전하다는 최소한의 보장받을 수 있음.
- CORS: Cross-Origin Resource Sharing, 교차 출처(다른 출처) 리소스 공유.
1-1. 출처(Origin)이란?
출처란 Protocol, Host, 포트를 모두 합친 것.
즉, URL에서 서버의 위치를 찾아가가 위해 필요한 가장 기본적인 것들을 합쳐놓은 것.
1-2. SOP(Same-Origin Policy)
- 같은 출처에서만 리소스를 공유할 수 있다라는 규칙을 가진 정책.
- 하지만 웹이라는 오픈된 환경에서는 다른 출처에 있는 리소스를 가져와서 사용하는 일이 굉장히 흔하기 때문에 무작정 막기 힘들다.
- 이에 따라 몇 가지 예외 조항을 두고, 이 조항에 해당하는 리소스 요청은 출처가 다르더라도 허용하기로 함.
- 그 중 하나가 CORS 정책을 지킨 리소스 요청임.
- 다른 출처로 리소스를 요청한다면 SOP 정책을 위반한 것이 됨.
- SOP의 예외 조항인 CORS 정책까지 지키지 않는다면 아예 다른 출처의 리소스를 사용할 수 없음.
1-3. 이러한 정책을 만드는 이유
- 출처가 다른 두 개의 어플리케이션이 마음대로 소통할 수 있는 환경이 위험하기 때문.
- 클라이언트 어플리케이션, 특히 웹은 사용자의 공격에 너무나도 취약하다.
- 개발자 도구만 열어도 각종 정보들을 아무런 제재 없이 열람할 수 있음.
- 다른 출처의 어플리케이션이 서로 통신하는 것에 대해 아무런 제약도 존재하지 않는다면, 공격자가 사용자의 정보를 탈취하기 너무나도 쉬워진다.
1-4. 같은 출처와 다른 출처의 구분
Scheme, Host, Port가 같으면 같은 출처라고 판단한다.
https://seok0301.com:80 과 같은 출처로 인정되는 예시
// 같은 출처. scheme, host, port가 같음.
https://seok0301.com/about
// 같은 출처. scheme, host, port가 같음.
https://seok0301.com/about?key=value
// 다른 출처. scheme이 다름.
http://seok0301.com
// 다른 출처. host가 다름.
https://seok0301.co.kr
// 다른 출처. host가 다름.
https://seok990301.com
// 다른 출처. port가 다름.
https://seok0301.com:8080
- 출처를 비교하는 로직은 서버에 구현된 것이 아니라 웹 브라우저에 구현되어 있음.
- 즉 웹 브라우저의 JavaScript 엔진에서 처리함.
- 만약 우리가 CORS 정책을 위반하는 리소스 요청을 하더라도,해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면, 서버는 정상적으로 응답하고, 이후 브라우저가 이 응답을 분석해서 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버리는 순서로 진행된다.
- 따라서 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다.
- 또한 CORS 정책을 위반하는 리소스 요청 때문에 에러가 발생했다고 해도, 서버 쪽 로그에는 정상적으로 응답을 했다는 로그만 남는다.
2. CORS 동작 원리
CORS가 동작하는 방식은 한 가지가 아니라 세 가지 시나리오에 따라 변경됨.
2-1. 기본적인 흐름
- 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때, HTTP 프로토콜을 사용해서 요청을 보냄.
- 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아서 보냄.
- 웹 서버는 이 요청에 대한 응답을 할 때, 응답 헤더의 Access-Control-Allow-Origin 값에 "이 리소스를 접근하는 것이 허용된 출처"를 내려줌.
- 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교한 후, 응답이 유효한지 아닌지 판단함.
2-2. 시나리오 1: Preflight Request
- 웹 어플리케이션을 개발할 때 가장 흔하게 마주치는 시나이로.
- 브라우저는 요청을 한 번에 보내지 않고, 예비 요청과 본 요청으로 나누어 서버에 전송함.
- Preflight: 브라우저가 본 요청을 보내기 전에 보내는 예비 요청.
- 예비 요청에는 HTTP 메소드 중 OPTIONS가 사용됨.
- 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것.
- 자바스크립트의 fetch API를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내림.
- 브라우저는 서버에게 예비 요청을 먼저 보냄.
- 서버는 이 예비 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내줌.
- 이후 브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책을 비교한 후, 이 요청을 보내는 것이 안전하다고 판단되면 같은 엔드포인트로 다시 본 요청을 보냄.
- 이후 서버가 이 본 요청에 대한 응답을 하면 브라우저는 최종적으로 이 응답 데이터를 자바스크립트에게 넘겨줌.
- 실제로 브라우저가 보낸 요청을 보면, 단순히 Origin에 대한 정보 뿐만 아니라, 예비 요청 이후에 보낼 본 요청에 대한 다른 정보들도 함께 포함되어 있다.
- 예비 요청에서 브라우저는 Access-Control-Request-Headers를 사용해서 본 요청에서 Content-Type 헤더를 사용할 것을 알려주거나, Access-control-request-Method를 사용해서 GET 메소드를 사용할 것을 서버에게 미리 알려줄 수 있다.
- 예비 요청에 대해서 서버는 응답을 보낸다.
- 서버가 보내준 응답 헤더에는 Access-Control-Allow-Origin 값이 존재한다.
- 이 값은 서버의 리소스에 접근이 가능한 출처를 나타낸다.
- 브라우저는 이 값과 요청을 보낸 출처를 비교하고, 만약 요청을 보낸 출처가 접근 가능한 출처가 아닐 경우 CORS 정책을 위반했다는 에러 메시지를 출력한다.
- 예비 요청에 대한 응답 코드는 200이고, 콘솔에는 에러가 표시될 수 있다.
- CORS 정책 위반으로 인한 에러는 예비 요청의 성공 여부와 별 상관이 없다.
- 브라우저가 CORS 정책 위반 여부를 판단하는 시점은 예비 요청에 대한 응답을 받은 이후이기 때문.
- 중요한 것은 예비 요청의 성공/실패 여부가 아니라, "응답 헤더에 유효한 Access-Control-Allow-Origin 값이 존재하는가" 이다.
- GET 메소드이면 큰 일이 일어나지 않겠지만, PATCH나 DELETE라면? CORS 에러가 발생했음에도 이미 서버는 요청을 모두 처리했기 때문에 지우면 안되는 데이터까지 수정하게 된다.
- Preflight Request를 사용한다면 해당 요청은 사전 요청이기 때문에 서버는 어떠한 행동도 취하지 않는다.
2-3. 시나리오 2: Credentialed Request
- 기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다.
- 이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 것이 credentials 옵션이다.
- credentials 옵션의 값과 의미.
- same-origin(기본값): 같은 출처 간 요청에만 인증 정보를 담을 수 있음.
- include: 모든 요청에 인증 정보를 담을 수 있음.
- omit: 모든 요청에 인증 정보를 담지 않음.
- 만약 same-origin이나 include 같은 옵션을 사용해서 리소스 요청에 인증 정보가 포함된다면, 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin만 확인하는 것이 아니라, 더 빡빡한 검사 조건을 추가하게 됨.
- 만약 웹 서버가 Access-Control-Allow-Origin 값으로 모든 출처를 허용한다는 의미인 *로 설정했다면, 다른 출처에서 해당 서버로 리소스를 요청할 때 CORS 정책 위반으로 제약을 받지 않는다.
- 이러한 경우 localhost와 같은 로컬 개발 환경에서도 fetch API를 사용하여 마음대로 리소스를 요청하고, 받아올 수 있다.
- 만약 credentials 값을 변경하지 않고, 로컬 환경에서 다른 출처의 웹 서버로 리소스 요청을 보낸다면, 기본 값은 same-origin이기 때문에 인증 정보가 포함되지 않는다.
- 이에 따라 브라우저는 응답의 "Access-Control-Allow-Origin: *"라는 값만 보고, 안전한 요청으로 판단함.
- 만약 credentials 값을 include로 바꾸고, 로컬 환경에서 다른 출처의 웹 서버로 리소스 요청을 보낸다면, 브라우저는 동일 출처가 아님에 상관 없이 인증 정보가 포함된 요청을 보낸다.
- 이러한 상황에서 브라우저는, 인증 모드가 include일 경우 "Access-Control-Allow-Origin: *"을 사용하면 안 된다는 에러 메시지를 출력한다.
-credentials 값이 include인 경우, Access-Control-Allow-Origin에는 *를 사용할 수 없으며, 응답 헤더에는 반드시 Acces-Control-Allow-Credentials: true가 존재해야 한다.
3. CORS를 해결할 수 있는 방법
3-1. 서버에서 Access-Control-Allow-Origin 헤더에 알맞은 값 세팅
- 와일드 카드인 *을 사용하면 모든 출처에서 오는 요청을 받을 수 있음.
- 하지만 당연하게도 보안적으로 심각한 이슈가 발생할 수 있음.
- 따라서 허용할 출처를 명시하는 것이 좋음.
- Access-Control-Allow-Origin 헤더는 미들웨어에서 세팅하는 것이 편함.
- 백엔드 프레임워크의 경우에는 CORS 설정을 위한 세팅이나 미들웨러 라이브러리를 제공함.
3-2. proxy 설정
- proxy 서버란 컴퓨터 네트워크에서 다른 서버상의 자원을 찾는 클라이언트의 요청을 받아 중계하는 서버.
- 주로 보안상의 문제로 직접 통신을 송수신할 수 없는 상황에서 프록시를 이용하여 중계 통신함.
- 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해줌.
- webpack-dev-server를 사용해서 개발 환을 구축한 경우, 해당 라이브러리가 제공하는 proxy 기능을 사용.
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://seok0301.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
}
}
}
-
로컬 환경에서 /api로 시작하는 URL로 보내는 요청에 대해서 브라우저는 loalhost:8000/api로 요청을 보낸 것으로 알고 있지만, 사실 뒤에서 webpack이 https://seok0301.com으로 요청을 proxying해주기 때문에 마치 CORS 정책을 지킨 것처럼 브라우저를 속이면서 원하는 서버와 자유롭게 통신할 수 있다.
-
실제 프로덕션 환경에서도 클라이언트의 어플리케이션의 소스를 서빙하는 출처와 API 서버의 출처가 같은 경우에만 사용하는 것이 좋다.
-어플리케이션을 빌드하고 서버에 올리고 나면 더 이상 webpack-dev-server가 구동하는 환경이 아니기 때문.
출처
https://evan-moon.github.io/2020/05/21/about-cors/
https://fgh0296.tistory.com/16
https://velog.io/@kim-jaemin420/%EC%9B%B9%ED%8C%A9%EC%9B%B9%ED%8C%A9%EC%9D%B4%EB%9E%80-%EC%9B%B9%ED%8C%A9%EC%9D%B4-%ED%95%98%EB%8A%94-%EC%9D%BC%EA%B3%BC-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0
https://fullmoon1344.tistory.com/145