Node.js로 웹서버 만들기_쿠키와 세션

이애옹·2022년 12월 29일
0

Node.js

목록 보기
14/32

📝 쿠키 생성 후 브라우저에 넣기

클라이언트에서 요청을 보낼 경우 누가 요청을 보내는지 모르게된다. 물론, 요청을 보내는 IP 주소나 브라우저 정보를 받아올 수는 있지만, 여러 컴퓨터가 공통으로 IP주소를 가지거나, 한 컴퓨터를 여러 사람이 사용하는 경우가 있다.

이럴 경우 로그인을 구현해야 하는데, 로그인 구현을 위해서는 쿠키세션에 대한 이해가 필요하다. 로그인 한 후에는 새로고침을 하더라도 로그인된 상태가 유지되는데, 이것이 바로 쿠키세션을 이용했기 때문이다.

이 과정에서 클라이언트가 서버에게 사용자의 정보를 지속적으로 알려주는데, 서버는 요청에 대한 응답을 할 때 쿠키라는것을 같이 보내준다.

쿠키name=zero 처럼 단순한 '키-값'쌍인데, 서버로부터 쿠키가 오면 웹 브라우저는 쿠키를 저장해뒀다가 요청할때마다 쿠키를 동봉해서 보내준다. 서버는 요청에 들어 있는 쿠키를 읽어서 사용자가 누구인지 파악할 수 있다.

즉, 서버는 클라인언트에 요청자가 추정할 만한 정보를 쿠키로 만들어 보내고, 그 다음부터 클라이언트로부터 쿠키를 받아 요청자를 파악한다.

여기서 쿠키는, 요청과 응답의 헤더(Header)에 저장된다. 요청과 응답은 각각 헤더본문(body)를 가지게 된다.

  • server3.js
const http = require('http');

const parseCookies = (cookie = '') =>
    cookie
        .split(',')
        .map(v => v.split('='))
        .map(([k, ...vs]) => [k, vs.join('=')])
        .reduce((acc,[k,v]) => {
            acc[k.trim()] = decodeURIComponent(v);
            return acc;
        },{});

http.createServer((req,res) => {
    const cookies = parseCookies(req.headers.cookie);
    console.log(req.url,cookies);
    res.writeHead(200,{'Set-Cookie':'mycookie=test' });
    res.end('Hello Cookie'); 
})
    .listen(8082, () =>{
        console.log('8082번 포트에서 대기중입니다!');
    });
    
  • 콘솔

    8082번 포트에서 서버 대기중입니다!

parseCookies라는 함수가 생성되었다.
쿠키는 name=zero;year=1994 처럼 문자열 형식으로 오기 때문에, {name:'zero', year:'1994'} 처럼 객체 형식으로 바꿔주는 함수를 구현한것이다.

createServer 메서드의 콜백에서는 제일 먼저 req 객체가 담겨 있는 쿠키를 분석한다.(쿠키는 req.headers.cookie에 들어있다.req.headers는 요청의 헤더를 의미한다.)

다음으로 응답의 헤더에 쿠키를 기록해야하므로 res.writeHead 메서드를 사용했다.
첫번째 인자로는 200이라는 상태코드를 넣었고(200은 성공을 의미) 두번째 인자로는 헤더의 내용을 입력한다.
Set-Cookie는 브라우저에 다음과 같은 값의 쿠키를 저장하라는 의미이며, 실제로 응답을 받은 브라우저는 mycookie=test라는 쿠키를 저장한다.

이제 브라우저창에 실행 주소를 입력하면, req.urlcookies 변수에 대한 정보를 로깅한다.(req.url은 주소의 path와 search 부분을 알려줌)

  • 화면 구현

  • 콘솔

    / { '': '' }
    /favicon.ico { mycookie: 'test' }

(실행 결과가 다르면 쿠키 삭제 후 재시도)

콘솔창의 결과를 보면, 요청은 한번만 보냈는데 두 개의 결과가 기록되어있다.

첫번째 요청에서는 쿠키에 대한 정보가 없다고 나오고, 두번째 요청에서는 { mycookie: 'test' } 가 기록되어있다.

여기서 favicon이란 웹사이트 탭에 보이는 이미지를 말한다.

브라우저에서는, 파비콘이 뭔지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보낸다. 현재 코드에서는 HTML에 파비콘 정보를 입력하지 않았으므로, 브라우저가 추가로 favicon.ico를 요청한 것 이다.

첫번째 요청을 보내기 전에는 브라우저가 어떠한 쿠키 정보도 가지고 있지 않았지만, 서버는 응답의 헤더에 mycookie=test라는 쿠키를 심으라고 브라우저에게 명령했다. 따라서 브라우저는 쿠키를 심었고, 두번째 요청에서 헤더에 쿠키가 들어있음을 확인할 수 있게되었다!!!

HTTP 상태 코드?
브라우저는 서버에서 보내주는 상태코드를 보고 요청의 성공/실패 여부를 확인하는데, 대표적인 상태코드는 다음과 같다.

  • 2XX : 성공을 알리는 상태코드
  • 3XX : 리다이렉션(다른 페이지로 이동)을 알리는 상태 코드
  • 4XX : 요청 오류를 나타내는 상태코드
  • 5XX : 서버 오류를 나타내는 상태코드

헤더와 본문
요청과 본문은 모두 헤더와 본문을 가지고 있다.
헤더는 요청 또는 응답에 관한 정보를 가지고 있고, 본문은 서버와 클라이언트간 주고받을 데이터를 가지고 있는 곳이다.

쿠키는 부가적인 정보이기 때문에 헤더에 저장된다.


크롬 개발자 도구의 Network 탭에서 요청과 응답을 살펴볼 수 있다. F12를 누른 뒤, 네트워크 버튼을 누르면 확인 할 수 있다.

그리고 127.0.01을 클릭한 뒤, 헤더(머리글) 버튼을 클릭하면,

다음과 같이 헤더 정보를 확인할 수 있다.
응답 헤더의 Set-Cookie를 통해 요청 헤더에 쿠키가 심어진 걸 확인할 수 있다.

쿠키 탭에서 쿠키 정보도 확인할 수 있다.


📝 쿠키 식별하기

아직까지는 단순히 쿠키만 심었을 뿐, 그 쿠키가 누구인지 식별하지 못하고 있다.

해당 문제를 해결하기 위해 다음과 같은 두 파일을 같은 폴더안에 생성해주었다.

  • server4.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>쿠키&세션 이해하기</title>
</head>
<body>
    <form action="/login">
        <input id="name" name="name" placeholder="이름을 입력하세요"/>
        <button id="login">로그인</button>
    </form>
</body>
</html>
  • server4.js
const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
  cookie
    .split(';')
    .map(v => v.split('='))
    .reduce((acc, [k, v]) => {
      acc[k.trim()] = decodeURIComponent(v);
      return acc;
    }, {});

http.createServer((req, res) => {
  const cookies = parseCookies(req.headers.cookie);
  if (req.url.startsWith('/login')) {
    const { query } = url.parse(req.url);
    const { name } = qs.parse(query);
    const expires = new Date();
    expires.setMinutes(expires.getMinutes() + 5);
    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
    });
    res.end();
  } else if (cookies.name) {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(`${cookies.name}님 안녕하세요`);
  } else {
    fs.readFile('./WebServer_Test/Test1/server4.html', (err, data) => {
      if (err) {
        throw err;
      }
      res.end(data);
    });
  }
})
  .listen(8083, () => {
    console.log('8083번 포트에서 서버 대기중입니다!');
  });

주소가 /login으로 시작하는 경우와 /로 시작하는경우 두가지로 나눠서 작성되었다.

1) 주소가 /login으로 시작하는 경우 : urlquerystring 모듈로 각각 주소와 주소에 딸려오는 query를 분석하며, 쿠키의 만료시간을 지금으로부터 5분으로 설정했다.

302 응답코드(임시이동), 리다이렉트 주소와 함께 쿠키를 헤더에 넣는다. 브라우저는 이 응답코드를 보고 페이지를 해당 주소로 리다이렉트하게된다. 헤더에는 한글을 설정할 수 없으므로 name 변수를 encodeURIComponent 메서드로 인코딩했다.

2) 주소가 /으로 시작하는 경우(또는 그외) : 먼저, 쿠키가 있는지 없는지를 확인한다. 쿠키가 없다면 로그인 할 수 있는 페이지(server4.html)를 보내준다. 쿠키가 있다면 로그인한 상태로 간주하여 인사말을 보내준다. res.end 메서드에 한글이 들어가면 인코딩 문제가 발생하므로, 헤더에 Content-Typetext/html; charset:utf-8;으로 설정해주었다.

  • 쿠키명 = 쿠키값 : 기본 쿠키값 (mycookie=test 또는 name=zero 와 같이 설정)

  • Expires=날짜 : 만료기한, 이 기한이 지나면 쿠기가 제거된다.(기본값은 클라이언트 종료 기간까지)

  • Max-age=초 : Expires와 비슷하지만 날짜 대신 초를 입력한다.(Expires 보다 우선)

  • Domain=도메인명 : 쿠키가 전송될 도메인을 특정한다.(기본값은 현재 도메인)

  • Path=URL : 쿠키가 전송될 URL을 특정한다.(기본값은 '/'으로, 이 경우 모든 URL에서 쿠키를 전송한다.)

  • Secure : HTTPS일 경우에만 쿠키가 전송된다.

  • HttpOnly : 설정 시 자바스크립트에서 쿠키에 접근할 수 없다. 쿠키 조작 방지를 위해 설정

다음과 같이 쿠키에는 각종 옵션을 추가할 수 있다.

  • 콘솔

    8083번 포트에서 서버 대기중입니다!

  • 실행 화면 1

    • 쿠키가 없을 경우
  • 실행 화면 2

    • 쿠키가 있을 경우

📝 세션을 통해 정보 보호하기

원하는대로 요청 처리가 완료 되었지만, 한가지 문제가 있다!

이 처럼 응용 프로그램(Applicatio) 탭에서 쿠키가 노출되어 있기 때문에 쿠키가 조작될 위험이 있다. 따라서 이름 같은 민감 정보를 쿠키에 넣는것은 적절하지않다.

다음과 같이 코드를 입력해주쟈

  • server5.js
const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
  cookie
    .split(';')
    .map(v => v.split('='))
    .reduce((acc, [k, v]) => {
      acc[k.trim()] = decodeURIComponent(v);
      return acc;
    }, {});

const session = {};

http.createServer((req, res) => {
  const cookies = parseCookies(req.headers.cookie);
  if (req.url.startsWith('/login')) {
    const { query } = url.parse(req.url);
    const { name } = qs.parse(query);
    const expires = new Date();
    expires.setMinutes(expires.getMinutes() + 5);
    const randomInt = +new Date();
    session[randomInt] = {
      name,
      expires,
    };
    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `session=${randomInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
    });
    res.end();
  } else if (cookies.session && session[cookies.session].expires > new Date()) {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(`${session[cookies.session].name}님 안녕하세요`);
  } else {
    fs.readFile('./WebServer_Test/Test1/server4.html', (err, data) => {
      if (err) {
        throw err;
      }
      res.end(data);
    });
  }
})
  .listen(8084, () => {
    console.log('8084번 포트에서 서버 대기중입니다!');
  });

먼저 포트 번호가 바뀌었고, 쿠키에 이름을 담아서 보내는 대신 randomInt라는 임의의 숫자를 보냈다.

사용자의 이름과 만료 시간은 session이라는 객체에 대신 저장했다.

이제 cookie.session이 있고, 만료기한을 넘기지 않았다면 session 변수에서 사용자 정보를 가져와서 사용한다.

  • 콘솔

    8084번 포트에서 서버 대기중입니다!

작동 화면은 위와 동일하지만,

응용 프로그램(Applicatio) 탭에서 더 이상 사용자의 정보가 보이지 않는다~

이 방식이 바로 세션이다. 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통이 가능하다. (세션아이디는 꼭 쿠키를 이용해 주고받아야하는건 아니지만, 그게 제일 간단해서 많이 사용한다고 한다.)

물론 실제 배포용 서버에서 저런 식으로 세션을 변수에 저장하면, 서버가 멈추거나 재시작되면 초기화되기때문에 사용하면 안된다. 또한, 서버의 메모리가 부족하면 더 이상 세션을 저장하지 못하기 때문에, 보통은 데이터베이스에 넣어둔다고 한다.

또한, 해당 코드는 쿠키 악용에 취약하고, 세션 아이디 값이 공개되어 누출될 위험이 있기 때문에 실제 서비스에는 사용불가능 하다고 한당~~~

profile
안녕하세요

0개의 댓글