1. CORS란
교차 출처 리소스 공유(Cross-origin Resource Sharing, CORS)는 추가 HTTP헤더를 사용하여, 한 출처에서 실행중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
여기서 origin(출처)란
Scheme(protocol), Host(domain), Port로 구성된다. 예를 들어 https://www.google.com/maps 라는 주소가 있다면
protocol : https://
host : www.google.com
port : :443
이다.
위와 같이 출처는 서버의 위치를 찾아가기 위해 가장 기본적인 구성요소라고 보면 된다.
여기서 같은 출처와 다른 출처의 구분
같은 출처라 하면 두 URL의 구성요소 중에 Scheme, Host, Port 이 3가지가 동일하며 된다.
예를 들면 https://google.com/maps:90 라는 출처를 예로 들면
http:// 이라는 것과 google.com/maps 그리고 port :90 번 포트를 사용하고 있다는 것만 같다면 나머지는 전부 다르더라도 같은 출처로 인정한다.
여기서 중요한 것은 이러한 출처 비교하는 로직이 서버에 구현된 것이 아닌 브라우저에서 구현되어 있는 스펙이라는 것이다.
만약 우리가 CORS 정책을 위반 하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답하고, 이후 브라우저가 이 응답을 분석해서 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그 응답을 무시한다.
즉, CORS는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다. 또한 CORS 정책을 위반하는 리소르 요청 때문에 에러가 발생했다고 해도 서버 쪽 로그에는 정상적으로 응답을 했다는 로그만 남기 때문에, CORS가 돌아가는 방식을 정확히 모른다면 에러 트레이싱에 난항을 겪을 것이다.
2. CORS의 동작 방식
요청 전송 :
기본적으로 웹 클라이언트 애플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 된다. 이 때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아 보낸다.
Origin : http://whereiserror.tistory.com
프리플라이트 요청 (Preflight Request, 선택적 단계):
브라우저는 특정 조건에 해당하는 요청에 대해 사전에 서버에 OPTIONS 메서드를 사용하여 프리플라이트 요청을 보낼 수 있다. 이는 실제 요청 전에 서버가 요청을 받아들일 수 있는지 확인하는 단계이다.
응답 처리 :
서버는 받은 요청에 대한 응답을 할 때, 응답 헤더에 Access-Control-Allow-Origin 값을 설정하여 '이 리소스에 접근하는 것이 허용된 출처'를 명시한다. 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교한다.
1. Access-Control-Allow-Origin 값이 '*'이라면, 모든 출처에서 접근이 허용된 것을 의미한다.
2. Access-Control-Allow-Origin 값이 요청한 출처와 일치한다면, 브라우저는 응답을 받아 처리한다.
3. Access-Control-Allow-Origin 값이 요청한 출처와 일치하지 않는다면, 브라우저는 CORS 정책 위반이라고 판단하여 응답을 무시하고 접근을 차단한다.
추가적인 제어 :
CORS는 추가적인 보안 및 제어 기능을 제공하기 위해 다양한 헤더를 사용할 수 있다. 예를 들어, 요청하는 출처에 대해 특정한 허용 메서드나 헤더, 인증 등을 설정할 수 있다.
3. CORS의 처리 방식
1. Preflight Request
프리플라이트(preflight) 방식은 일발적인 웹 애플리케이션 개발시 주로 마주치는 시나리오이다. 이 방식은 시나리오에 해당하는 상황일 때 브라우저는 요청을 한 번에 보내지 않고, 예비 요청과 본 요청으로 나누어서 서버로 전송한다.
이때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 부르며, 이 예비 요청에는 HTTP 메서드 중 OPTIONS 메서드가 사용된다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.
이 과정을 간단한 플로우 차트로 나타냈다.
자바스크립트의 fetch API 를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내리면 브라우저는 서버에게 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내준다.
이후 브라우저는 자신이 보낸 예비 요청과 서버가 응답에 담아준 허용 정책을 비교한 후, 이 요청을 보내는 것이 안전하다고 판단되면 엔드포인트로 다시 본 요청을 보내게 된다. 이후 서버가 이 본 요청에 대한 응답을 하면 브라우저는 최종적으로 이 응답 데이터를 자바스크립트에게 넘겨준다.
플라이트 요청이 발생하는 조건
1. 요청 메서드: GET, POST, HEAD 외에 다른 메서드를 사용하는 경우
2. 커스텀 헤더: 일부 특수한 헤더를 사용하는 경우, 예를 들어 "Content-Type"이나 "Authorization"과 같은 사용자 지정 헤더
3. CORS에 의해 요구되는 특정 헤더를 설정한 경우, 예를 들어 "Content-Type" 헤더를 "application/json"으로 설정하는 경우
2. Simple Request
단순 요청(Simple Request)라고 부른다.
단순 요청은 예비 요청을 보내지 않고 바로 서버에게 본 요청을 보낸 후 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin과 같은 값을 보내주면 그때 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다. 즉 프리플라이트와 단순 요청 시나리오는 전반적으로 로직 자체는 같지만, 예비 요청의 존재 유무만 다르다.
하지만 아무 때나 단순 요청을 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있다.
단순 요청 발생하는 조건
1. 요청 메서드: GET, POST, HEAD 중 하나의 메서드를 사용하는 경우
2. 커스텀 헤더: 일반적인 커스텀 헤더를 사용하지 않는 경우
3. CORS에 의해 요구되는 특정 헤더를 설정하지 않는 경우
단순하게 예시를 들자면
1. 브라우저에서 서버로의 간단한 GET 요청은 단순 요청이다. 예를 들면 이미나 문서의 파일을 가져오는 GET 요청이 있다.
2. POST 요청 (application/x-www-form-urlencoded): 브라우저에서 폼 데이터를 서버로 전송하는 POST 요청도 단순 요청이다. 단순 요청으로 인코딩된 폼 데이터는 "application/x-www-form-urlencoded" 미디어 타입으로 전송된다.
3.일부 헤더가 있는 GET 요청: 일부 헤더를 추가하지만 일반적인 커스텀 헤더를 사용하지 않는 GET 요청도 단순 요청으로 분류된다.
3. Credentialed Request
인증 관련 헤더를 포함할 때 사용하는 요청이다. 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 기본적으로 요청에 담지 않으므로, credentials 옵션을 변경하지 않고서는 cookie를 주고 받을 수 없다.
하지만 이를 사용하게 된다면 브라우저는 인증정보(예: 쿠키, HTTP 인증헤더)를 요청과 함께 포함시킬 수 있다.
이 시나리오는 CROS의 기본적인 방식이라기 보다는 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법이다.
이 옵션에는 총 3가지 값을 사용할 수 있다. 각 값들이 가지는 의미는 다음과 같다.
same-origin (기본값) : 같은 출처 간 요청에만 인증 정보를 담을 수 있다
include : 모든 요청에 인증 정보를 담을 수 있다
omit : 모든 요청에 인증 정보를 담지 않는다
만약 same-origin이나 include와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Arrow-Origin만 확인 하는 것이 아닌 좀 더 검사 조건을 추가하게 된다.
이를 사용하기 위해서는 몇가지 추가적인 설정이 필요하다.
1. 서버 설정: 서버는 요청을 처리할 때 Access-Control-Allow-Credentials 헤더를 true로 설정하여 Credentialed Request를 허용한다고 명시해야 한다. 이를 통해 브라우저는 인증 정보를 요청에 포함시킬 수 있는지 결정한다.
2. 클라이언트 설정: 클라이언트에서는 요청을 보낼 때 withCredentials 옵션을 true로 설정하여 Credentialed Request를 사용한다고 명시해야 한다. 이를 통해 브라우저는 요청에 인증 정보를 포함시키도록 한다.
Credentialed Request를 사용하면 브라우저는 인증 정보를 요청에 포함시키므로, 서버는 요청을 보낸 사용자를 식별하고 해당 사용자의 인증 상태를 확인할 수 있다. 이는 보안적인 요구사항이 있는 애플리케이션에서 유용하며, 인증된 사용자에게 특정 리소스에 접근 권한을 부여하는 등의 제어가 가능하다.
Credentialed Request를 사용하기 위해서는 서버와 클라이언트 모두 설정을 올바르게 해야 하며, 서버에서는 인증된 요청을 처리할 수 있는 로직을 구현해야 한다. 또한, 주의할 점은 Credentialed Request를 사용하면 Access-Control-Allow-Origin 헤더의 값은 "*"를 사용할 수 없으며, 요청한 출처에 대한 명시적인 허용이 필요하다는 점이다.
4. CORS 해결 방안
1. Access-Control-Allow-Origin 응답 헤더 세팅
서버 측 응답에서 접근 권한을 주는 헤더를 추가하여 해결
App.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*"); // 모든 도메인
res.header("Access-Control-Allow-Origin", "https://example.com"); // 특정 도메인
});
2. CORS 모듈 사용
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" });
});
3. webpack-dev-server proxy 기능
리액트 개발환경에서, 서버쪽 코드를 수정하지 않고 해결할수도 있다. 아래와 같이 프록시 속성을 설정하면, 서버에서 해당 요청을 받아준다.
// 프록시 쓰지 않았을때
// 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 에러가 발생하지 않는다.
webpack-dev-server의 proxy 기능을 사용하면 리액트 개발 환경에서 서버 측 코드를 수정하지 않고도 CORS 에러를 해결할 수 있다. 프록시를 설정하면 webpack-dev-server가 클라이언트의 요청을 중간에서 가로채고 프록시 서버를 통해 실제 서버로 요청을 전달한다.
프록시 설정 예시인 devServer 객체 내의 proxy 속성을 통해 프록시를 설정할 수 있다. 위의 코드 예시에서 /api 경로로 들어오는 요청은 target 속성에 설정된 domain.com으로 프록시됩니다. changeOrigin: true 옵션을 사용하면, 요청의 Origin 헤더를 변경하여 서버에서 요청을 받을 때 같은 도메인으로 인식하도록 할 수 있다.
4. package.json에 proxy 값을 설정
create-react-app으로 생성한 프로젝트에서는, package.json에 proxy 값을 설정하여 proxy 기능을 활성화 하는 방법이 있다.
// package.json
{
//...
"proxy": "http://localhost:4000"
}
'HTML & CSS' 카테고리의 다른 글
간단한 css 실습하기 (1) (0) | 2024.07.25 |
---|---|
HTML 기본 구조 및 CSS 기본 가이드 (0) | 2024.07.25 |
2023.05.08 CSS 개본개념 (0) | 2023.05.09 |
2023.05.06 CSS 기본 개념 (0) | 2023.05.08 |
2023.05.04 CSS 기본 개념 (0) | 2023.05.05 |