[TIL] Web server 기초, mini node server 만들기 과제

ㅜㅜ·2022년 10월 13일
1

Today I learn

목록 보기
32/77
post-thumbnail

CORS

(Cross-Origin Resource Sharing) : 교차 출처 리소스 공유

  • SOP(Same-Origin-Policy) = 동일 출처 정책은 '같은 출처의 리소스만 공유 가능 하다'는 정책이다.
    여기서 말하는 '출처'는 프로토콜, 호스트, 포트의 조합으로 하나라도 다르면 동일 출처가 아니게 된다.
    이 정책은 잠재적으로 해로울 수 있는 문서를 분리해 공격 받을 수 있는 경로를 줄여주지만, 실제 개발에서는 많은 다른 출처의 리소스들을 사용하게 되기 때문에 CORS가 필요해진다.

  • CORS는 추가 HTTP 헤더를 이용해 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 '권한' 부여하도록 브라우저에 알려준다.

(CORS policy 관련 에러가 뜰 때 CORS 설정을 통해 서버 응답 헤더에 'Access-Control-Allow-Origin'을 작성해주면 접근 권한 얻을 수 있음.)



CORS의 3가지 동작 방식

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 값만 허용)
  1. Credentialed Request (인증 정보를 포함한 요청)
    : 요청 헤더에 인증 정보를 담아 보내는 요청.
    출처가 다른 경우 별도 설정을 하지 않으면 쿠키 보낼 수 없음. (민감한 정보라) 이 경우 프론트, 서버 양측 모두 CORS 설정이 필요.
    🟡 프론트 측 : 요청 헤더에 withCredentials : true 를 넣어줘야 함.
    🟡 서버 측 : 응답 헤더에 Access-Control-Allow-Credentials : true 를 넣어줘야 함.
(서버 측에서 Access-Control-Allow-Origin 을 설정할 때, 모든 출처를 허용한다는 뜻의 와일드카드(*)로 설정하면 에러가 발생. 인증 정보를 다루는 만큼 출처를 정확하게 설정해주어야 함.)



CORS 설정 방법

  • node.js 서버
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");
})
  • Express(프레임워크) 서버
    : cors 미들웨어를 사용해 보다 더 간단하게 CORS 설정해줄 수 있음.
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" });
});




🫠 과제 : Mini Node Server

HTTP 트랜잭션 해부 문서

  • createServer를 이용해 웹 서버 객체를 만들 수 있었음.
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}`);
});
  • 공식 문서에서는 구조분해할당으로 미리 request에 매서드, URL, 헤더에 해당하는 값만 넣어 변수처럼 사용할 수 있게 해뒀다. 실제로 과제를 풀 때는 아래 코드가 없어도 작동은 되었다.
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)
}
  • 공식 문서 예시에서는 에러 처리를 처음에 한 번, 리스폰스를 문자열로 바꾸면서 한 번 해줬는데 과제에서는 에러 처리를 뭉뚱그려 조건문의 else로 처리해줬다.
//🤔공식문서 예시
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는 응답을 끝내주는 녀석이라 여러 개를 쓸 수 없다.

profile
다시 일어나는 중

0개의 댓글