본문 바로가기

Node.js/[Node.js]

[Node.js] 쿠키와 세션

클라이언트에서 서버로 보내는 요청은 누가 요청을 보내는지 모른다는 치명적인 단점이 있다.

물론 IP 주소나 브라우저의 정보를 받아올 수는 있는데 여러 컴퓨터가 공통으로 IP 주소를 가지는 경우도 있다.

 

이를 해결하기 위해 우리가 흔히 아는 로그인을 구현하면 된다. 이때 알아야 할 것이 쿠키세션이다.

 


쿠키(cookie) 

    쿠키란??

  • 서버에 의해 클라이언트(브라우저)에 저장되는 데이터 파일이다.
  • 유효기간이 있으며 name=seokjun 과 같이 '키=값' 의 쌍이다.
  • Session cookies는 유효 기간을 포함하지 않는다. 대신 브라우저가 열려있는 동안에만 저장되고, 브라우저가 닫히면 cookies는 영구적으로 삭제된다.  ( 은행 유저들의 자격 증명 저장 )
  • Persistent cookies는 유효 기간을 가진다. 브라우저가 종료되어도 유효 기간이 끝날 때 까지는 계속 정보가 남아있다. ( 유저의 로그인 정보 )

 

    쿠키의 원리

  • 서버가 클라이언트에게 요청에 대한 응답을 할때 쿠키를 보낸다. (Response Header Set-Cookie 속성을 사용)
  • 서버로부터 쿠키가 오면 클라이언트는 쿠키를 저장해두었다가 다음에 요청할 때마다 쿠키를 Request Header에 넣어서 자동으로 서버에 전송한다.
  • 그러면 서버는 요청에 들어있는 쿠키를 읽어서 사용자가 누구인지 파악한다.

    **쿠키는 요청과 응답의 헤더에 저장된다!!**

 

쿠키의 원리

 

    쿠키의 사용 예

  • 사이트의 로그인 기능
  • 쇼핑몰의 장바구니 기능
  • 팝업 창에서 "오늘 더 이상 이 창을 보지 않음" 체크란

 

    쿠키 구현

const http = require('http');

http.createServer((req, res) => {
	console.log(req.url, req.headers.cookie);
	res.writeHead(200, { 'Set-Cookie': 'mycookie=test' });
	res.end('Hello Cookie');
})
	.listen(3000, () => {
		console.log('3000번 포트에서 서버 대기중입니다');
	});
  • 서버가 응답의 헤더에 mycookie=test 라는 쿠키를 심으라고 브라우저에게 명령(Set-Cookie)했다.
  • 두 번째 요청부터는 요청의 헤더에 쿠키가 들어있다.

서버가 응답의 헤더에 쿠키를 set 함.

 

 

    간단한 로그인 기능 구현

쿠키를 설정하는 방법을 알았으니 이를 활용해서 간단하게 로그인 기능을 통해 사용자를 식별하는 방법을 알아보자.

 

*cookie2.html*

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>쿠키&세션 이해하기</title>
    </head>
    <body>
        <form action="/login">
            <input id="name" name="name" placeholder="이름을 입력하세요" />
            <button id="login">로그인</button>
        </form>
    </body>
</html>
  • form태그를 이용한 GET 요청
  • GET 요청인 경우 데이터는 url 뒤에 querystring으로 들어감 (ex. /login?name=seokjun)

 

*cookie2.js*

const http = require('http');
const fs = require('fs').promises;
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(async (req, res) => {
	const cookies = parseCookies(req.headers.cookie); // { name : 'seokjun' }
	//주소가 /login 으로 시작하는 경우
	if (req.url.startsWith('/login')) {
		const { query } = url.parse(req.url); //parse 메소드는 url 문자열을 url 객체로 변환하여 리턴   
		const { name } = qs.parse(query);
		console.log(req.url); //  /login?name=seokjun
		console.log(query);//     name=seokjun
		console.log(name);//      seokjun
		const expires = new Date();
		//쿠키 유효 시간을 현재시간 + 5분으로 설정.
		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/plain; charset=utf-8' });
		res.end(`${cookies.name}님 안녕하세요`);
	} else {
		try {
			const data = await fs.readFile('./cookie2.html');
			res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
			res.end(data);
		} catch (err) {
			res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
			res.end(err.message);
		}
	}
})
	.listen(3000, () => {
		console.log('3000번 포트에서 서버 대기중');	
	})
  • parseCookies 함수는 name=seokjun과 같은 문자열인 쿠키를 객체 형식으로 바꿔주는 함수. { name: 'seokjun' } 으로 바뀜.
  • 로그인 버튼을 누르는 순간  /login?name=seokjun  이런식으로 데이터가 url 뒤에 붙어서 전달.
  • 응답의 헤더에 쿠키를 기록(res.writeHead) 할 때 HTTP 상태코드 중에서 302를 사용해서 리다이렉션.
  • 리다이렉션을 통해 로그인 이후에 '/' 이 주소로 다시 돌아감. (  Location: '/'  )
  • Set-Cookie를 할때 HttpOnly 설정 시 자바스크립트에서 쿠키에 접근할 수 없다. 쿠키조작을 방지하기 위해 꼭 설정하자. (로그인을 위해 사용하는 쿠키에서는 필수)

 

 

 

로그인 이전

 

 

로그인 이후에 브라우저에 사용자 정보 저장

새로고침을 해도 그대로 로그인이 유지된다.

지금 개발자도구의 Application탭에서 보이는 것처럼 쿠키가 노출되어 있다. 조작의 위험도 다분하다.

따라서 이름과 같은 민감한 개인정보를 쿠키에 넣어두는 것은 적절하지 않다.

 

여기서 세션이 등장한다.

 

 


세션 (Session)

  세션이란??

  • 브라우저에 정보를 저장하는 쿠키와 달리 서버에 사용자 정보를 저장한다.
  • 서버에서는 클라이언트를 구분하기 위해 세션 ID를 부여하며, 브라우저가 서버에 접속해서 브라우저를 종료할 때 까지 인증상태를 유지한다.
  • 정보를 서버에 두기 때문에 쿠키보다 보안에 좋지만, 사용자가 많아질수록 서버 메모리를 많이 차지하게 된다.
  • 클라이언트가 Request를 보내면, 해당 서버가 클라이언트에게 유일한 세션 ID를 부여한다. 아래 코드의 uniqueInt가 그 예시이다.

 

const http = require('http');
const fs = require('fs').promises;
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(async (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 uniqueInt = Date.now();
    session[uniqueInt] = {
      name,
      expires,
    };
    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
    });
    res.end();
  // 세션쿠키가 존재하고, 만료 기간이 지나지 않았다면
  } else if (cookies.session && session[cookies.session].expires > new Date()) {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`${session[cookies.session].name}님 안녕하세요`);
  } else {
    try {
      const data = await fs.readFile('./cookie2.html');
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(data);
    } catch (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end(err.message);
    }
  }
})
  .listen(3000, () => {
    console.log('3000 포트에서 서버 대기 중입니다!');
  });
  • 위의 cookie2.js에서 createServer 부분만 일부 수정되었다.
  • 쿠키에 이름을 담아서 클라이언트에 보내는 대신, uniqueInt 라는 숫자값을 보냈다.
  • 사용자의 이름과 만료 시간은 session이라는 객체에 저장된다.

 

 

쿠키를 사용해서 세션 아이디를 주고받는다.

 

  • 사용자 정보는 서버에 저장하였고, 노출된 쿠키에는 개인정보가 아닌 세션 아이디만 노출이 되었다.
  • 서버가 멈추가나 재시작되면 메모리에 저장된 변수가 초기화되므로 실제 서버에서는 세션을 위와 같이 변수에 저장하지 않는다.
  • 또한 서버의 메모리가 부족하면 세션을 저장하지 못하는 문제도 생긴다.
  • 따라서 보통은 세션을 레디스(Redis)나 멤캐시드(Memcached) 같은 데이터베이스에 넣어둔다.

 

오늘 공부한 코드는 그저 개념을 공부하기 위한 코드이므로 보안에 매우 취약하다.

실제로는 다른 사람의 코드(모듈)를 사용하거나, express-session 모듈을 사용한다.

 

express 모듈을 추후에 공부한 뒤에 포스팅하도록 하겠다.