SOP(Same-Origin-Policy) = 동일 출처 정책은 '같은 출처의 리소스만 공유 가능 하다'는 정책이다.
여기서 말하는 '출처'는 프로토콜, 호스트, 포트의 조합으로 하나라도 다르면 동일 출처가 아니게 된다.
이 정책은 잠재적으로 해로울 수 있는 문서를 분리해 공격 받을 수 있는 경로를 줄여주지만, 실제 개발에서는 많은 다른 출처의 리소스들을 사용하게 되기 때문에 CORS가 필요해진다.
CORS는 추가 HTTP 헤더를 이용해 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 '권한' 부여하도록 브라우저에 알려준다.
1. Preflight Request (프리플라이트 요청)
: 실제 요청 보내기 전 'OPTIONS' 매서드 이용해 사전 요청을 보내서 해당 출처 리소스에 접근 권한이 있는지 확인하는 것. (권한 없을 시 CORS에러, 실제 요청 전달되지 않음)
=> 실제 요청을 처음부터 통째로 보내는 것보다 리소스 측면에서 효율적.
=> CORS에 대비되어 있지 않은 서버를 보호할 수 있다. 이런 서버는 다른 출처에서 들어오는 요청에 대한 대비가 되어 있지 않은데, 이런 서버에 요청을 보내면 응답을 보내기 전 우선 요청을 처리하게 된다. 클라이언트와 서버 사이에 있는 브라우저는 서버의 응답을 받은 뒤에야 CORS 권한이 없다는 것을 알게 된다. 예를 들어 DELETE, PUT 같은 권한 없이 실행되어서는 안 되는 Cross-Origin 요청이 실행되는 것도 preflight를 통해 방지 가능.
2. Simple Request (단순 요청)
: 특정 조건 만족되면 프리플라이트 요청 생략하고 요청 보내는 것
✅ 조건
- GET, HEAD, POST 요청 중 하나
- 자동으로 설정되는 헤더 외, Accept, Accept-Language,Content-Language, Content-Type 헤더의 값만 수동으로 설정할 수 있음.
(Content-Type 헤더에는 application/x-www-form-urlendcoded, multipart/form-data, text/plain 값만 허용)
withCredentials : true
를 넣어줘야 함.Access-Control-Allow-Credentials : true
를 넣어줘야 함.Access-Control-Allow-Origin
을 설정할 때, 모든 출처를 허용한다는 뜻의 와일드카드(*)로 설정하면 에러가 발생. 인증 정보를 다루는 만큼 출처를 정확하게 설정해주어야 함.)const http = require('http');
const server = http.createServer((request, response) => {
// 모든 도메인
response.setHeader("Access-Control-Allow-Origin", "*");
// 특정 도메인
response.setHeader("Access-Control-Allow-Origin", "https://codestates.com");
// 인증 정보를 포함한 요청을 받을 경우
response.setHeader("Access-Control-Allow-Credentials", "true");
})
const cors = require("cors");
const app = express();
//모든 도메인
app.use(cors());
//특정 도메인
const options = {
origin: "https://codestates.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" });
});
const http = require('http')
const server = http.createServer((request, response) => {
//여기서 작업이 진행됨
}).listen(4999)
server가 받는 request, response도 객체임.
요청을 실제로 처리하기 위해서는 listen 매서드가 server 객체에서 호출되어야 하는데, 과제에서는 다른 방법으로 구현되어 있었다.(공식문서와 다른 모양들이 있어 읽어보는데 좀 시간이 걸렸다.) 미리 PORT라는 변수를 만들어 포트 번호를 할당해두고, ip라는 변수에는 localhost라는 문자열을 할당했다. 콘솔로 찍어주기 위해 이렇게 만든 것 같다.
const PORT = 4999;
const ip = "localhost";
//위의 변수들은 전역 변수
server.listen(PORT, ip, () => {
console.log(`http server listen on ${ip}:${PORT}`);
});
const {headers, method, url} = request
Stream, ReadableStream, WritableStream, Buffer
:스트리밍은 우리가 흔히 생각하는 유튜브 스트리밍~ 과 비슷한 느낌이라고 생각하면 되는데, 스트리밍을 할 때 chunk로 데이터를 받아오고 body라는 빈 배열에 우리는 계속해서 이런 chunk를 push 해준다. 이 chunk를 콘솔에 찍어보니 buffer가 나오는 것을 확인할 수 있는데, 이런 Buffer를 하나로 모아주는 것이 Buffer.concat()이고, 모인 것을 문자열로 .toString 매서드를 이용해 바꾸면, 그때서야 받은 데이터가 제대로 나타난다.
오류가 발생했을 때도 요청의 body를 받아올 때처럼 error 이벤트가 발생한 것이고, 이 이벤트를 핸들러로 다룰 수 있다.
.on은 jquery이고, 이벤트리스너 같은 역할을 한다. addEventListener를 jquery를 사용해 .on 매서드로 대체한 것!
request.on('error', (err) => {
console.error(err.stack)
}
//🤔공식문서 예시
const http = require('http');
http.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request.on('error', (err) => {
console.error(err);//에러처리
}).on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
response.on('error', (err) => {
console.error(err);//에러처리
});
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
// 주의: 위 두 줄은 다음 한 줄로 대체할 수도 있습니다.
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();
// 주의: 위 두 줄은 다음 한 줄로 대체할 수도 있습니다.
// response.end(JSON.stringify(responseBody))
});
}).listen(8080);
//🤔과제
const http = require("http");
const PORT = 4999;
const ip = "localhost";
const server = http.createServer((request, response) => {
if (request.method === "OPTIONS") {
response.writeHead(200, defaultCorsHeader);
response.end();
}//preflight 요청이지만 어쨌든 요청이므로 응답필요!
if (request.method === "POST" && request.url === "/upper") {
let body = [];
request
.on("data", (chunk) => {
body.push(chunk);
})
.on("end", () => {
body = Buffer.concat(body).toString();
response.writeHead(200, defaultCorsHeader);
response.end(body.toUpperCase());
});
} else if (request.method === "POST" && request.url === "/lower") {
let body = [];
request
.on("data", (chunk) => {
body.push(chunk);
})
.on("end", () => {
body = Buffer.concat(body).toString();
response.writeHead(200, defaultCorsHeader);
response.end(body.toLowerCase());
});
} else {
response.statusCode = 400;
response.end();
}//else에서 statusCode로 에러 표시
});
server.listen(PORT, ip, () => {
console.log(`http server listen on ${ip}:${PORT}`);
});
const defaultCorsHeader = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Accept",
"Access-Control-Max-Age": 10,
};
이해가 잘 안 됐던 부분 중 하나가 왜 preflight가 아닌 실제 요청들에게 응답해줄 때도 헤더 부분에 CORS를 (ex:response.writeHead(200, defaultCorsHeader)) 넣어줘야 하는지였는데, 구글링 + MDN 공식 문서를 살펴보니 preflight 요청 후 응답할 때 객체에 origin이 존재하는 것과 마찬가지로, 실제 요청에 응답을 해줄 때에도 객체에 origin이 존재하기 때문에 origin을 계속 밝혀주어야 하는 것 같았다...🫠
언제나 하나의 요청에는 하나의 응답이 있어야 하는데, 그래서 실제로 요청에 대한 응답으로 response.end('응답!')와 같은 코드를 작성했었다. 그리고 preflight요청도 하나의 요청이기 때문에 response.end()를 잊지 말고 작성해주어야 한다.
response.write를 여러번 써서 응답을 나눠 보내더라도 하나로 묶어서 단일 응답으로 취급한다.
response.end는 응답을 끝내주는 녀석이라 여러 개를 쓸 수 없다.