📌 CORS
교차-출처 리소스 공유(Cross-Origin Resource Sharing)
합법적으로 리소스 공유를 할 수 있도록 만들어진 메커니즘으로 한 출처에서 실행중인 웹 애플리케이션이 다른 출처의 자원에 접근할때 이와 관련한 에러가 발생한다.
MDN에 따르면, 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
protocol
, host
, port
를 총칭하며 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들이다.
protocol
: 통신규약, 사용자가 서버에 접속 시 통신할 방식 정의
host
: IP에 이름을 부여한 도메인 네임 혹은 서버 컴퓨터 IP
port
: 해당 IP주소가 가리키는 PC에 접속할 수 있는 통로나 서버 어플리케이션을 특정할 때 사용하는 번호로 이해할 수 있으며 80번인 http
과 443번인 hhtps
프로토콜은 각 프로토콜의 기본포트로 생략이 가능하다.
참고로
path
: 컴퓨터 내부에 있는 디렉토리의 파일을 가리킴. 즉, 자원의 경로를 의미한다.
query string
: 데이터를 전달하는 데 사용, 쿼리 스트링의 시작은 ?
이고, 각 key-value쌍은 &
로 구분된다.
CORS(교차 출처 리소스 공유)
와 더불어 웹 생태계에서 다른 출처로부터 리소스 요청을 제한하는 것과 관련된 두 가지 정책 중의 하나로 한 Origin으로부터 로드된 document 혹은 script가 다른 Origin의 리소스와 상호작용 할 수 있는 방법을 제한하는 중요한 보안 메커니즘이다.
즉, 자신과 같은 origin(출처)에서만 리소스를 공유할 수 있다는 규칙이다. 브라우저에서 다른 서버에서 요청할 경우에 해당되고, 브라우저 없이 서버 간 통신을 할 때는 이 정책이 적용되지 않는다.
만약 다른 출처의 어플리케이션이 서로 통신하는 것에 대해 아무런 제약도 존재하지 않는다면 악의를 가진 사용자가 소스 코드를 보고 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting)와 같은 방법을 사용하여 정보를 탈취할 수 있기 때문에 이러한 정책이 존재한다.
CORS
는 다른 출처의 리소스가 필요한 경우, SOP
를 우회하기 위한 여러가지 방법 중 가장 권장되는 방법이다.
브라우저와 서버간에 요청을 바로 보내지 않고 허용되는 메소드가 무엇인지, 허용되는 Origin인지 등을 preflight request
를 통해서 먼저 확인한다.
단계
1. 요청 메소드가 GET, POST 중 둘중 하나인지 파악한다.
2. Content-Type과 Custom HTTP Header를 파악한다.
3. OPTIONS 요청을 통해서 서버가 적절한 Access-Control을 가졌는지 확인한다.
4-1. 만약 적절한 Access-Control을 가졌다면 실제 XHR을 트리거한다.
4-2. 적절하지 못한 Access-Control을 가졌다면 Error를 발생시킨다.
preflight request를 생략하고 서버에 바로 실제 요청을 보낸 후 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS 정책 위반 여부를 검사하는 방식입니다. preflight request를 생략하려면 아래와 같은 조건들이 모두 만족되어야 합니다.
조건
1. 요청 메소드가 GET, HEAD, POST 중 하나인지 파악한다.
2. User Agent가 자동으로 설정한 Header외에, 수동으로 설정할 수 있는 헤더는 오직 Fetch 명세에서 "CORS-safelisted request-header"로 정의한 Header(Accept
,Accept-Language
,Content-Language
,Content-Type (아래 추가 요구 사항에 유의)
,DPR
,Downlink
,Save-Data
,Viewport-Width
,Width
) 뿐인지 파악한다.
3. Content-Type 헤더가 application/x-www-form-urlencoded
, multipart/form-data
, text/plain
중 하나여야 한다.
참고로
요즘 HTTP 요청은 application/json 또는 text/xml로 이뤄지기 때문에 대부분 Content-Type 헤더 조건을 만족시키지 못해서 보통 preflight request
가 이뤄진다.
클라이언트가 서버에 요청할 때 자격 인증 정보(Credential)를 담아서 요청할 때 사용되는 방식이다. 여기서 말하는 자격 인증 정보는 세션 ID가 저장되어 있는 쿠키나 Authorization 헤더에 설정하는 토큰 값 등을 의미한다.
기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 데이터를 함부로 요청 데이터에 담지 않도록 설정되어 있다. 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다. 이 옵션에는 세가지 값이 있는데 각각 다음과 같은 의미를 같는다.
same-origin
: 같은 출처 간 요청에만 인증 정보를 담을 수 있음
include
: 모든 요청에 인증 정보를 담을 수 있음
omit
: 모든 요청에 인증 정보를 담을 수 없음
또한 서버에서도 응답 헤더를 다음과 같이 설정해줘야 한다.
응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정한다.
응답 헤더의 Access-Control-Allow-Origin을 *(와일드카드)로 설정하면 안된다.
응답 헤더의 Access-Control-Allow-Methods를 *로 설정하면 안된다.
응답 헤더의 Access-Control-Allow-Headers를 *로 설정하면 안된다.
참고로 credentialed request
역시 preflight request
가 선행된다.
📌 주의해야 할 점
origin 체크는 서버가 아니라 브라우저에서 이뤄진다. preflight request가 없이 실제 요청으로 바로 넘어가는 경우에는 이미 서버 쪽에서는 브라우저 쪽의 요청에 의해 어떠한 작업이 일어난 후에 CORS 에러가 브라우저 쪽에서 보여질 수도 있다. CORS 에러가 떴으니 요청이 서버 쪽에 안 먹혔겠지 생각하고 있다가는 큰 코 다칠 수 있는 상황이다. 서버는 요청에 대해 응답을 반환했을 수 있다. 다만 브라우저에서 그 응답을 받을 수 없는 상황일 수 있다.
서버측 응답에서 접근 권한을 주는 헤더를 추가하여 해결
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*"); // 모든 도메인
res.header("Access-Control-Allow-Origin", "https://example.com"); // 특정 도메인
});
const cors = require("cors");
const app = express();
app.use(cors());
아무 옵션없이 설정하면 모든 cross-origin 요청에 대해 응답이므로, 특정 도메인이나 특정 요청에만 응답하게 옵션을 설정하는 것이 좋다.
const options = {
origin: "http://example.com", // 접근 권한을 부여하는 도메인
credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
optionsSuccessStatus: 200, // 응답 상태 200으로 설정
};
app.use(cors(options));
app.get("/example/:id", cors(), function (req, res, next) {
res.json({ msg: "example" });
});
리액트 개발환경에서, 서버쪽 코드를 수정하지 않고 해결할 수도 있다. 아래와 같이 프록시 속성을 설정하면, 서버에서 해당 요청을 받아준다.
// 프록시 쓰지 않았을때
// localhost:8080(클라이언트 측) --X (CORS)--> domain.com (서버 측)
// 프록시를 설정 후
// localhost:8080(클라이언트 측) --O 프록시가 설정된 Webpack Dev Server--> domain.com (서버 측)
module.exports = {
devServer: {
proxy: {
"/api": {
target: "domain.com",
changeOrigin: true,
},
},
},
};
중간의 프록시 서버 덕분에, domain.com 서버에서는 같은 도메인(domain.com)에서 온 요청으로 인식하여 CORS 에러가 발생하지 않는다.
create-react-app 으로 생성한 프로젝트에서는, package.json 에 proxy 값을 설정하여 proxy 기능을 활성화 하는 방법도 있다.
{
//...
"proxy": "http://localhost:4000"
}
http-proxy-middlewqre 라이브러리를 사용하면 로컬환경에 한해 CORS에러를 해결할 수 있는데,
해당 라이브러리가 요청 출처를 프록싱해준다
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
createProxyMiddleware('/users', {
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
}),
);
};
레퍼런스
https://ingg.dev/cors/,
https://bskyvision.com/entry/CORS%EC%99%80-%EA%B4%80%EB%A0%A8-%EC%9E%88%EB%8A%94-preflight-request%EB%9E%80