코드스테이츠/코드스테이츠 @ 개발 복습

[코드 스테이츠] 56일차, "8주차 복습(2) - CORS, 미들웨어"

Je-chan 2021. 9. 12. 20:28


[ 오늘의 TODO ]

  1. 코드 스테이츠) 목~금 내용 복습
    // CORS
    // 미들웨어
  2. 패스트 캠퍼스) 인강 3개 이상 듣기 // optional
  3. 생활) 물 1L 이상 마시기
  4. 생활) 수-토-일 운동 

 


[ 오늘의 복습 ]

1. CORS

  브라우저 보완에 관해 CORS, XSS, CSRF 등이 존재한다. 그중에서도 우리는 널리 사용되고 있는 CORS 에 대해서 확인해보도록 하자. 

1) 등장 배경

  CORS 가 등장하기 전, 서버는 클라이언트라는 파일을 갖고 있었다. 유저가 서버에 요청을 하면 서버 안에 있는 클라이언트를 받아서 통신하거나 클라이언트 안에 Static 하게 존재하던 데이터를 받아왔다. 이런 과정은 서버에서 요청자에게 일방적으로 제공하는 과정이다. 이런 일방적인 관계까 유지될 수 있는 이유는 서버가 항상 우리에게 원하는 것을 줄 것이라는 신뢰에서 비롯된다. 그런 신뢰를 뒷받침해줄 수 있었던 건 우리가 요청을 하는 서버는 한 개뿐이고, 그 한 개의 서버에서 우리가 원하는 데이터를 갖고 있기 때문이다.

 

  여기서 서버를 Origin이라고 부르자. 현재는 하나의 Origin 에게서만 데이터를 받는가? 그렇지 않다. 최근 웹 애플리케이션은 여러 서버에 요청을 하고 그 서버"들"에서 받아온 데이터"들"을 사용한다. 즉 하나의 Origin 의 하나의 데이터만을 공유받는 것이 아니라 여러 Origin 으로부터 여러 데이터를 공유받게 된 것이다. 이런 과정을 Cross Origin Resource Sharing 이라고 하며 줄여서 CORS 라 한다.

 

  단, 우리가 지금부터 말하는 CORS는 그렇게 여러 데이터를 공유하기 위한 "체제"의 의미로 사용된다. 초기 브라우저는 보안 상의 이유로 cross-origin HTTP 요청을 제한했다. 하지만, 최근의 웹 애플리케이션에서 cross-origin HTTP 요청은 거의 필수적인 작업이 되었고 브라우저에서는 cross origin 을 허용하게 된다. 이전에도 언급했든 보안 상의 이유가 존재한다. 그래서 브라우저가 자발적으로 브라우저 애플리케이션을 보호하기 위한 조치를 취하게 됐고 그 조치를 CORS 라고 부른다. 한국 MDN 공식 문서에서는 CORS 를 "한 출처(origin)에서 실행 중인 웹 애플리케이션이 다른 출처(origin)의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다." 번역한 것이기에 말이 좀 어려울 수 있으나, CORS 라는 건 원래 실행 중인 한 Origin에서 다른 Origin 의 데이터를 사용할 수 있는지 검사하고 통과되면 요청한 데이터에 접근할 수 있도록 권한을 부여해주는 것이라고 생각하면 된다. 

 

2) 서버에서 CORS 받아들이기

  서버의 입장에서는 수많은 클라이언트들을 만나게 된다. 금융의 일을 생각해보자. 은행은 누구에게나 열려 있다. 하지만 누가 대출 요청을 하게 된다면 그 사람의 신원을 조회하고 신용 등급을 확인해야 한다. 아무에게나 대출을 해줄 수 없기 때문이다. 마찬가지로 데이터를 제공하는 서버의 입장에서 서버를 제공해도 괜찮은 사람이 맞는지를 검사해야할 필요가 있다. 클라이언트 요청은 HTTP messages 를 통해 들어온다. 그렇다면, 서버는 HTTP messages 를 읽기 전에 먼저 이 클라이언트에게 서버를 줘도 되는지 확인 작업을 거쳐야 한다. 그 작업을 다음 밑의 코드처럼 설정을 한다. 

 

const defaultCorsHeader = {
  'access-control-allow-origin' : '*'
  // 어떤 origin에게만 데이터를 제공해주겠냐는 의미다. *은 "전체"를 의미
  // 즉, 모든 origin에게 데이터를 제공해주겠다는 것
  // 만약 * 이 아닌 localhost:5000 으로 되어있다면 localhost:5000 에서의 요청만 받아준다.
  
  'access-control-allow-methods' : 'GET, POST, PUT, DELETE, OPTIONS'  
  // 어떤 Method만 받아주겠냐는 의미다.
  // 설정된 Method 이외의 다른 Method 는 받아주지 않는다
  
  'access-control-allow-headers' : 'Content-type, Accept'  
  // Headers 에서 어떤 내용만을 받아주겠냐는 의미다.
  
  'access-control-allow-max-age' : 10
  // 결과를 캐시할 수 있는 최대 시간(단위는 초)을 의미한다.
  // 파이어 폭스는 최대 24시간(86400), 크롬(v76 이전)은 10분(600) 까지, v76은 2시간(7200) 까지다
  // 캐시에 대한 내용은 아직 자세하게 배우지 않았다. Section 3 에서 다룬다고 한다.
}

 

  이렇게 작업을 거친 것들은 각 메소드별로 response 를 보내줄 때 이걸 인자로 받는다. 

 

if (req.method === 'OPTIONS') {
  res.writeHead(200, defaultCorsHeader);
  res.end();
}
  
 if (req.method === 'POST') {
   // Post Body 내용으로 data를 가공함
   res.writeHead(201, defaultCorsHeader);
   res.end(data);
});

 

  3) 클라이언트에서 요청 보내기

  클라이언트에서 요청(request)을 보낸다는 건 서버로부터 원하는 데이터(source)를 받아오는 것을 의미한다. 요청 중에는 Preflight Request 라는 것이 존재한다. 여기서 간단하게 설명하자면, 은행 대출을 받기 위해 서류를 제출하듯이 클라이언트는 서버로부터 어떤 요구를 하기 전에 Preflight Request (직역하면 비행하기 전에 보내는 요구, 의역하면 사전 요청 작업)를 보낸다 

 

  Simple Requests

  simple request 라는 건 말 그대로 간단한 request 로 Prefilght Request 를 보내지 않아도 되는 요청이다. 이 request 에는 몇 가지 조건이 필요하다.

 

  1.  GET, HEAD, POST 중 하나의 HTTP 메소드에 속해야 한다

  2. 요청 헤더에 User Agent 에서 자동으로 세팅한 HEAD 만으로 이뤄져 있어야 한다.
    - Accept
    - Accept-language
    - Content-langugae
    -Content-type

  3. Content-type 은 다음 카테고리 중 하나여야만 한다.
    - application/x-www-form-urlencoded
    - multipart/form-data
    - text/plain

  MDN 에서 사용된 예시를 가져와보자. 

 

const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

// send 는 클라이언트인 브라우저에게 요청을 보내달라고 요구하는 것
// send 를 통해 브라우저가 서버에 Reuqest를 보내고
// 서버로부터 Response를 받는다.
// 브라우저는 자바스크립트 코드에 onload나 onerrorfh Response 내용을 전달해준다

 

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example

 

  위의 코드와 HTTP messages 는 현재 Simple Request 의 조건에 들어맞는다. 따라서 보는 것처럼 클라이언트가 서버에 요청할 때 따로 Preflight Request 가 발생하지 않는다.

 

 

  Preflight Request 

  위에서도 잠깐 언급했듯이 클라이언트가 서버로부터 무언가를 요청하기 전에 보내는 사전 작업이다. 서버는 우리가 앞서 작성했던 기준인 defaultCorsHeader 로 지금 요청받는 요구 사항이 적합한지를 판별하고 그에 따른 응답(Response)로 작용한다. 그리고 만일 여기에서 통과가 된다면 그다음에 본래 클라이언트가 하고 싶었던 요청을 보내어 작업할 수 있게 된다.

 

  MDN 공식 문서의 예시를 가져오면

 

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');

// 메소드는 POST 로 첫 번째 Simple Reuqest 조건은 통과하지만
// user agent 가 자동으로 생성한 HEAD를 변형하고 있으므로 두 번째 조건에 위배된다
// 마지막 조건에서도 요구하는 Content-type 이 아니므로 조건에 위배된다.
// 즉, 이 요청은 Simple Reuquest가 아니며 Preflight를 필요로 한다.

OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

 

  비록, POST라는 요청 하나만을 보냈으나 Preflight 가 필요해지면 자동으로 OPTIONS 메소드로 보내게 된다. 

 

 

 

2. 미들웨어

  보통 Node.js 환경에서 서버를 구축할 때, require('http') 를 통해서 한다. 하지만 Node.js 에는 http 보다 더 편리한 프레임워크 express 를 제공한다.

 

1) 미들웨어란?

  express 가 자랑하는 능력은 "미들웨어" 다. 미들웨어는 response 를 보내기까지 거치는 모든 과정 하나하나를 의미한다. 즉 공장으로 따지자면 response 라는 결과물을 만들어 내기까지 거치는 공정 하나하나를 미들웨어라고 보면 된다. 

 

  미들웨어를 사용하는 이유는 다음과 같다.

  1. 모든 요청에 url, 메소드를 확인하고자 할 때
  2. POST 요청 등에 포함된 body를 쉽게 구조화하기 위해 (body-parser)
  3. 모든 요청/응답에 CORS 헤더를 붙이기 위해 (cors)
  4. 요청 헤더에 사용자 인증 정보가 담겼는지 확인하고자 할 때

  그렇다면 미들웨어가 어떻게 사용되는지 알아보자. 

 

https://expressjs.com/ko/guide/writing-middleware.html

 

  위의 미들웨어는 endpoint가 '/' 이고 클라이언트로부터 GET 요청을 받았을 때 적용되는 미들웨어다. req는 request, res는 response 의 약자고 next는 현재 미들 웨어를 실행하고 나서 다음 미들 웨어를 실행하도록 한다. 미들웨어 내부에서는 아무런 작업을 하지 않고 next() 함수를 호출해서 다음 미들웨어로 데이터를 전달하는 역할을 맡는다. 만약 endpoint가 아니라 모든 요청에 동일한 미들웨어를 적용하기 위해선 app.use() 를 사용하면 된다.

 

2) 자주 사용되는 미들웨어

  미들웨어의 종류는 이미 저 위의 이유들에서 괄호 안에 넣어놨었다. body-parser 라는 미들웨어는 요청에 담긴 body를 쉽게 구조화할 때 사용하는 것이고 cors 는 모든 요청/응답에 CORS 헤더를 넣기 위함이다

 

 body-parser

  먼저 npm install body-parser 를 해주고 아래와 같이 작성한다.

 

const express = require('express')
const app = express()

const bodyParser = require('body-parser')
const bodyJson = bodyParser.json()


app.post('/shopping/users', jsonParser, function (req, res) {
  // req.body에는 JSON의 형태로 담겨 있다.
})

 

  

  그런데 노드 버전이 높다면 이런 body-parser 가 기본적으로 내장돼 있다. 

 

const express = require('express')
const app = express()

app.use(express.json())

app.post('/shopping/users', jsonParser, function (req, res) {
  // req.body에는 JSON의 형태로 담겨 있다.
})

  이렇게만 사용해도 위의 bodyParser 와 동일한 효과를 지니게 된다. 

 

  

  cors

  일반적인 Node.js 코드에 CORS 헤더를 붙이기 위해 writeHead에 defaultCorsHeaders 로 정의한 것을 집어넣었다. defaultCorsHeaders 라는 것을 만들어서 넣는데도 시간이 오래 걸리고 options 에 대한 라우팅도 따로 구현해야만 했다. 

 

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
};

// 생략
if (req.method === 'OPTIONS') {
  res.writeHead(201, defaultCorsHeader);
  res.end()
}

 

  하지만 cors 라는 미들웨어를 사용하면 이런 과정을 매우 간단하게 처리할 수 있다. 먼저 npm install cors 를 해준 다음 다음과 같이 코드를 작성하면 된다. (예시는 현재 공식 문서 사이트에서 받아오고 있다)

 

const cors = require('cors')

// 생략

// 1. 모든 요청에 대해 CORS 허용하고자 할 때
// app.use라는 미들웨어 자체가 cors를 실행하도록 만든다

app.use(cors()) 


// 2. 특정 요청에 대해 CORS 허용하고자 할 때
// 특정 요청 안에만 cors() 를 인자로 넣어준다.

app.get('/products/:id', cors(), function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for a Single Route'})
})

// 만약 본인이 Access

 

  단, 만약에 cors 에 default 인 내용만을 사용하고자 하는 것이 아니라 그 안에 무언가 변형을 주고 싶다고 하면 아래와 같이 객체로 속성 값을 바꿔주면 된다

 

var express = require('express')
var cors = require('cors')
var app = express()

var corsOptions = {
  origin: 'http://example.com', // origin의 내용을 바꾸는 것. example.com 에서만 요청을 허락한다.
  optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}

app.get('/products/:id', cors(corsOptions), function (req, res, next) {
  // cors () 안에 corsOptions 라는 객체를 넣어 속성 값을 변화시킨다.
  res.json({msg: 'This is CORS-enabled for only example.com.'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

 

    express 를 사용하면 일반 Node.js 환경에서 처럼 options 에 대한 라우팅을 반드시 작성할 필요는 없다. 하지만, options를 사용해야만 하는 경우가 존재한다. 받아들이는 HTTP verb, 즉 HTTP 메소드가 GET, HEAD, POST 가 아닌 경우 (DELETE 나 클라이언트가 커스터마이징한 경우)에는 app.options 를 사용한다.

 

var express = require('express')
var cors = require('cors')
var app = express()

app.options('/products/:id', cors()) // enable pre-flight request for DELETE request
app.del('/products/:id', cors(), function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for all origins!'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})