
Cross-Origin Resource Sharing(교차 출처 리소스 공유)
하나의 출처(Origin)에서 실행 중인 웹 앱이 다른 출처의 자원(Resource)에 접근할 수 있도록 허용하는 메커니즘
Protocol, Host, Post 세 가지로 구성된다.
이 중 하나라도 다르면 다른 출처로 간주한다.
예를 들어서 https://example.com:80 이라는 주소가 있다고 가정한다.
| URL | 출처 | 결과 | 이유 |
|---|---|---|---|
https://example.com:80/app | https://example.com:80 | 동일 출처 | 경로(Path)만 다름 |
http://example.com:80 | https://example.com:80 | 다른 출처 | 프로토콜(http vs https)이 다름 |
https://example.com:80 | https://www.example.com:80 | 다른 출처 | 호스트(example.com vs www.example.com)가 다름 |
https://example.com:80 | https://example.com:8080 | 다른 출처 | 포트(:80 vs :8080)가 다름 |
웹 브라우저는 기본적으로 SOP(Same-Origin Policy, 동일 출처 정책)을 따른다.
이는 보안상의 이유로, 다른 출처의 리소스를 함부로 가져오지 못하게 막는 정책이다.
만약 이 정책이 없다면, 악의적인 사이트가 사용자의 개인 정보를 쉽게 탈취할 수 있을 것이다.
그러나 요즘 웹 앱은 API 서버, 이미지 서버 등 다양한 출처의 리소스를 사용하는 경우가 많다.
이때 SOP 때문에 리소스를 가져올 수 없는 문제가 생긴다.
따라서 CORS는 서버에 특정 설정을 추가함으로써, 신뢰할 수 있는 다른 출처의 요청을 허용해 준다.
HTTP 헤더를 통해 동작한다.
Origin이라는 필드를 추가하여 자신의 출처를 알린다.Origin 헤더를 확인한다.Access-Control-Allow-Origin 필드를 추가하여 응답한다.Access-Control-Allow-Origin이 있고, 그 값이 자신의 출처와 일치하면 리소스를 정상적으로 처리한다. 만약 이 헤더가 없거나 출처가 일치하지 않으면, 브라우저는 보안 오류를 발생시키고 리소스를 차단한다.정말 간단하다. 이미 미들웨어로 구현이 되어있어서 가져다 쓰기만 하면 된다.
import express from 'express';
import cors from 'cors';
const app = express();
// cors 미들웨어를 사용
app.use(cors());
// 특정 출처만 허용하고 싶을 경우
/*
const corsOptions = {
origin: 'https://allowed-domain.com', // 허용할 출처
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
*/
// API 엔드포인트를 정의
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS 요청 성공!' });
});
// 서버를 3000번 포트에서 실행
app.listen(3000, () => {
console.log('서버가 3000번 포트에서 실행 중입니다.');
});
내부 동작을 확인하기 위해 Node.js의 net 모듈을 사용해서 low level에서 직접 구현한다.
프리플라이트 요청을 처리하고, HTTP 프로토콜을 직접 해석하고 적절한 CORS 헤더를 포함한 응답을 수동으로 만들어야 한다.
브라우저가 본 요청(e.g. PUT, DELETE)을 보내기 전에, 서버가 이 요청을 안전하게 처리할 수 있는지 확인하기 위해 먼저 보내는 OPTIONS 메서드 요청이다.
일종의 "사전 탐색"이라고 생각할 수 있다.
브라우저는 단순 요청(Simple Request)이 아닐 경우에 프리플라이트 요청을 보낸다.
아래 조건 중 하나라도 해당하면 비-단순 요청으로 간주되어 프리플라이트가 필요하다.
GET, HEAD, POST가 아닌 다른 메서드 (e.g. PUT, DELETE, PATCH 등)Authorization, Content-Type: application/json)Content-Type 헤더: application/x-www-form-urlencoded, multipart/form-data, text/plain 이외의 값을 가지는 경우예를 들어 프론트엔드에서 비단순 요청을 보내면, 브라우저는 해당 요청을 보내기 전에 먼저 OPTIONS 메서드 요청을 서버로 보낸다.
프리플라이트 요청은 CORS 설정과 관계 없이 단순 요청이 아닐 때 항상 발생한다.
OPTIONS 메서드인지 확인OPTIONS 요청 응답OPTIONS 요청이면, HTTP 204 No Content 상태 코드와 함께 CORS 허용 헤더들을 응답으로 보낸다.const startLine = lines[0]; // 첫 번째 줄 (e.g. GET / HTTP/1.1)
if (!startLine) return;
// 첫 줄을 공백 기준으로 나눠서 메서드와 경로 추출
const [method, requestPath, version] = startLine.split(' ');
if (method === 'OPTIONS') {
// CORS 프리플라이트 요청 처리
const response = [
`${version} 200 OK`,
'Access-Control-Allow-Origin: *', // 모든 도메인에서의 접근 허용
'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS', // 허용된 HTTP 메서드
'Access-Control-Allow-Headers: Content-Type, Authorization', // 허용된 요청 헤더
'Access-Control-Max-Age: 86400', // 24시간 동안 프리플라이트 결과 캐시
'Content-Length: 0',
'',
].join('\r\n');
socket.write(response);
}
OPTIONS가 아닌 본 요청이 들어오면, 이 응답에도 반드시 Access-Control-Allow-Origin 헤더를 포함해서 보낸다.else if (method === 'GET' && requestPath === '/index') {
const body = loadTemplate('index.html');
// HTTP 응답 헤더를 규칙에 맞게 작성 (CORS 헤더 포함)
const response = [
`${version} 200 OK`,
'Content-Type: text/html; charset=utf-8',
`Content-Length: ${Buffer.byteLength(body)}`,
'Access-Control-Allow-Origin: *',
'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers: Content-Type, Authorization',
'', // 헤더와 본문 구분
body,
].join('\r\n');
socket.write(response);
}