본문 바로가기

Node.js

Node.js - 쿠키와 세션 (Cookie and Session)

쿠키에 관한 내용은 아래 글을 참고한다.

 

https://lgphone.tistory.com/65

 

쿠키와 세션

클라이언트에서 보내는 요청에는 큰 단점이 존재한다. 바로 누가 요청을 보내는지 모른다는 것이다. 물론 요청을 보내는 IP 주소나 브라우저의 정보를 받아올 수는 있다. 그러나 여러 컴퓨터가 �

lgphone.tistory.com

 

아래와 같은 코드를 작성하여 노드로 실행해 준다.

 

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(8083, () => {
    console.log('8083번 포트에서 서버 대기 중입니다!');
});

 

실행 후 localhost:8083에 들어가서 페이지를 확인해보면,

 

위와 같이 뜬다. 보면 req (request) 객체에 담겨있는 쿠키는 req.headers.cookie에 들어있다. req.headers는 요청의 헤더이므로, 쿠키가 요청과 응답의 헤더를 통해 오감을 확인할 수 있다.

 

응답의 헤더에 쿠키를 기록해야 하므로, res.writeHead 메서드를 사용했다. Set-Cookie는 브라우저에게 다음과 같은 쿠키를 저장하라는 의미이다. 실제로 응답을 받은 브라우저는 mycookie=test라는 쿠키를 저장한다. 또한 8083번 포트에 들어가서 페이지를 확인해보면 아래와 같은 쿠리 리스트가 뜨는 것을 볼 수 있다.

 

/ _ga=...; _ga_KT4F1VCYFG=...
/favicon.ico _ga=...; _ga_KT4F1VCYFG=...; mycookie=test

 

요청은 분명 한 번만 보냈는데 두 개가 기록되어 있다. 파비콘 (favicon) 은 브라우저가 파비콘이 뭔지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보낸다.

 

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

 

이제 쿠키로 사용자를 식별하는 방법을 알아보자.

 

cookie2.html 파일과 cookie2.js 파일을 아래와 같이 만들어준다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>쿠키&세션 이해하기</title>
</head>
<body>
    <form action="/login">
        <input id="name" name="name" type="text" placeholder="이름을 입력하세요."/>
        <button id="login">로그인</button>
    </form>
</body>
</html>

 

const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') => {
    return 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);

    // 주소가 login으로 시작할 떄
    if (req.url.startsWith('/login')) {
        const { query } = url.parse(req.url);
        const { name } = qs.parse(query); // parse query string
        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();
    } 
    // name 이라는 쿠키가 있는 경우
    else if (cookies.name) {
        res.writeHead(200, {
            'Content-Type': 'text/html; 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(500, {
                'Content-Type': 'text/html; charset=utf-8'
            });
            res.end(err.message);
        }
    }
}).listen(8084, () => {
    console.log('8084번 포트에서 서버 대기 중입니다.');
});

 

위 코드를 자세히 보면,

 

  1. parseCookies 함수는 쿠키를 자바스크립트 객체로 바꿔주는 함수이다. 쿠키는 key=value 형식의 문자열이다. 따라서 이를 객체로 바꾸어줄 함수가 필요한 것이다.
  2. 주소가 /login으로 시작할 경우, url과 querystring 모듈로 각각 주소와 주소에 딸려오는 query를 분석한다. 그리고 쿠키의 만료시간은 로그인 시점으로부터 5분 뒤로 설정해 놓았다.
  3. 헤더에는 한글을 설정할 수 없으므로, name 변수를 encodeURIComponent 메서드로 인코딩했다. 또한 Set-Cookie의 값에는 제한된 ASCII (아스키) 코드만 들어가야 하므로 줄바꿈을 넣으면 안된다.
  4. 그 외의 경우, 먼저 쿠키가 있는지 없는지를 확인하고, 없다면 로그인할 수 있는 페이지로 보낸다. 처음 방문한 경우에는 쿠키가 없으므로 coockie2.html이 전송된다. 쿠키가 있다면 로그인한 상태로 간주되며 따라서 인삿말을 보낸다.

8084번 포트로 열어보면,

위와 같이 잘 나타난다. 이름을 입력하고 로그인을 해보자.

이러한 페이지로 가졌다. 이제 새로고침을 눌러도 여전히 잘 뜬다. 쿠키에 내 정보가 저장되어 있으며, 이 쿠키의 정보를 내가 요청으로써 서버에 전달하면, 서버는 요청을 받아 쿠키를 보고 로그인이 되어있다고 판단 후, 위와 같은 페이지를 응답으로 다시 보내준다. 이제 크롬의 어플리케이션 (Application) 탭에서 이를 확인해보자.

위와 같이 쿠키에 내 정보가 저장되어 있는 것을 볼 수 있다. 그러나 이 경우 쿠키가 노출되어 있으며, 쿠키가 조작될 위험도 있다. 따라서 민감한 개인정보를 쿠키에 넣어두는 것은 적절치 못하다.

 

위에 작성했던 코드에서 Set-Cookie로 쿠키를 설정할 때 만료 시간 (Expires) 와 HttpOnly, Path 같은 옵션들을 부여했었다. 쿠키를 설정할 때에는 각종 옵션을 넣을 수 있으며, 옵션 사이에 세미콜론을 써서 구분하면 된다. 옵션들은 다음과 같다.

  • 쿠키명=쿠키값: 기본적인 쿠키의 값
  • Expires=날짜: 만료 기한. 기본값은 클라이언트 종료시 까지.
  • Max-age=초: Expires와 비슷하지만 대신 초를 입력할 수 있음. Expires보다 우선함.
  • Domain=도메인명: 쿠키가 전송될 도메인을 특정할 수 있다. 기본값은 현재 도메인이다.
  • Path=URL: 쿠키가 전송될 URL을 특정할 수 있다. 기본값은 '/'이고, 이 경우 모든 URL에서 쿠키를 전송할 수 있다.
  • Secure: HTTPS일 경우에만 쿠키가 전송된다.
  • HttpOnly: 설정 시 자바스크립트에서 쿠키에 접근할 수 없다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋다.

 

서버가 사용자 정보를 관리하도록 만들어보자. 위 코드를 아래와 같이 바꿔준다.

 

const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') => {
    return 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);

    // 주소가 login으로 시작할 떄
    if (req.url.startsWith('/login')) {
        const { query } = url.parse(req.url);
        const { name } = qs.parse(query); // parse query string
        const expires = new Date();
        // 쿠키와 유효시간을 현재 시간 + 5분으로 설정
        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();
    } 
    // name 이라는 쿠키가 있는 경우
    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/html; charset=utf-8'
            });
            res.end(err.message);
        }
    }
}).listen(8084, () => {
    console.log('8084번 포트에서 서버 대기 중입니다.');
});

 

바뀐 점은 세션이라는 객체 내에 관련 정보를 관리해준다는 것이다. 쿠키에는 이름 대신 uniqueInt라는 숫자 값만 보내졌다. 실제 정보는 서버 내의 session이라는 객체에 대신 저장함으로써, 이름과 같은 개인정보의 직접적 유출을 막았다.

 

이러한 방식이 세션이다. 서버에 사용자 정보를 저장하고, 클라이언트와는 세션 아이디로만 소통한다. 세션 아이디는 꼭 쿠키를 사용해서 주고받지 않아도 된다. 그러나 많은 웹사이트가 쿠키를 사용하여 주고 받으며, 이 방법이 가장 간단하기 때문이다. 세션을 위해 사용하는 쿠키를 세션 쿠키 (session cookie) 라고 부른다.

 

그러나 서비스를 새로 만들 때마다 쿠키와 세션을 직접 구현할 수는 없다. 게다가 위 코드로는 쿠키를 악용한 여러 가지 위협을 방어하지도 못한다. 위의 방식 역시 세션 아이디 값이 공개되어 있기 때문에, 누출될 경우 다른 사람이 악용할 가능성도 존재한다. 그러므로 안전한 사용을 위해 다른 사람들이 만든 검증된 코드를 사용하는 것이 좋다. 이에 대해선 추후에 다뤄보도록 하자.

 

출처

Node.js 교과서 개정 2판 - 길벗, 조현영