[nodejs] Cookie , Session
로그인을 구현하려면 안정성을 위해 꼭 쿠키와 세션에 대한 이해가 필요하다.
쿠키 - 유효기간이 있고 name=leekc와 같이 단순한 '키-값'이다.
서버로부터 쿠키가 오면, 웹 브라우저는 쿠키를 저장해뒀다가 다음에 요청할 때마다 쿠키를 동봉해서 보낸다.
서버는 요청에 들어 있는 쿠키를 읽어서 사용자가 누구인지 파악한다.
쿠키는 요청의 헤더에 담겨 전송된다.
브라우저는 응담의 헤더(Set-Cookie)에 따라 쿠키를 저장한다.
서버에서 직접 쿠키를 만들어 요청자의 브라우저에 넣어주는 코드.
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(8003, () => {
console.log('8083번 포트에서 서버 대기 중입니다!');
});
- 쿠키는 mycookie = test; year=1994처럼 문자열 형식으로 존재합니다. 쿠키 간에는 세미콜론을 넣어 각가을 구분합니다.
위의 코드는 단순히 쿠키를 심는 코드이고 그 쿠키가 사용자를 식별해주지는 못하고 있습니다.
아래 코드에서 사용자를 식별한 쿠키를 보내보겠습니다.
const http = require('http);
const fs = require('fs').promises;
const path = require('path');
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);
// 주소가 login으로 시작하느 경우
if (req.url.startsWith('/login')) {
const url = new URL(req.url, 'http://localhost:8084');
// name을 url에서 가져온다
const name = url.searchParams.get('name');
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
parseCookies 함수는 쿠키 문자열을 쉽게 사용하기위해 자바스크립트 객체 형식으로 바꾸는 함수.
로그인 요청(GET /login)을 처리하는 부분
form은 GET 요청인 경우 데이터를 쿼리스트링으로 보내기에 URL 객체로 쿼리스트링 부분을 분석.
쿠키의 만료시간을 지금으로 부터 5분뒤로 설정
res.writeHead - 302응답 코드, 리다이렉트 주소와 함께 쿠키를 헤더에 넣는다.
헤더에는 한글설정 불가 -> name 변수를 encodeURIComponent 메서드로 인코딩
'Set-Cookie' 값으로는 제한된 ASCII 코드만 들어가야 하므로 줄바꿈 x. 실제에서는 줄바꿈 x
// 위의 코드와 else if문으로 이어집니다
// 주소가 /이면서 name이라는 쿠키가 있는경우
if (rea.url.startsWith('/login')){
// ...
pass
}else if (cookies.name) {
res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8' });
res.end(`${cookies.name}님 안녕하세요`);
}else { // 주소가 /이면서 name이라는 쿠키가 없는 경우
try{
}catch (err){
res.writeHead(500, { 'Content-Type' : 'text/plain; charset=utf-8' });
res.end(err.message);
}
})
.listen(8084, () => {
console.log('8084번 포트에서 서버 대기 중입니다.');
});
먼저, 쿠키가 있는지 없는지를 확인합니다. 쿠키가 없다면 로그인할 수 있는 페이지를 보냅니다.
Set-Cookie로 쿠키를 설정할 때 만료 시간(Expires)과 httpOnly, Path 같은 옵션을 부여했습니다.
쿠키를 설정할 때는 각종 옵션을 넣을 수 있으며, 옵션 사이에 세미콜론(;)을 써서 구분하면 됩니다.
쿠키에는 들어가면 안되는 글자들이 있는데, 대표적으로 한글과 줄바꿈이 있습니다.
한글은 encodeURIComponent로 감싸서 넣습니다.
다음은 쿠키의 메서드인자 입니다.
- 쿠키명 = 쿠키값: 기본적인 쿠키의 값입니다. mycookie=test 또는 name=leekc와 같이 설정합니다.
- Expires=날짜 : 만료 기한입니다. 이 기한이 지나면 쿠키가 제거됩니다. 기본값은 클라이언트가 종료될 때 까지입니다.
- Max-age=초 : Expires와 비슷하지만 날짜 대신 초를 입력할 수 있습니다. 해당 초가 지나면 쿠키가 제거됩니다. Expires보다 우선합니다.
- Path=URL: 쿠키가 전송될 URL을 특정할 수 있습니다. 기본값은 '/'이고, 이 경우 모든 URL에서 쿠키를 전송할 수 있습니다.
- Secure: HTTPS일 경우에만 쿠키가 전송됩니다.
- HttpOnly: 설정 시 자바스크립트에서 쿠키에 접근할 수 없습니다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋습니다.
이렇게 쿠키를 통해 사용자를 식별할 수 있게 되었습니다.
하지만 이 방법은 상당히 위험합니다. 원하는대로 동작하기는 하지만 Applicatio 탭에서 보이는 것처럼 쿠키가 노출되어 있습니다. 또한 쿠키가 조작될 위험도 있습니다.
다음과 같이 코드를 변경(session 사용)해 서버가 사용자정보를 관리하도록 만듭니다.
// session.js
const http = require('http')
const fs = require('fs').promises;
const path = require('path');
const parseCookies = (cookie='') =>
cookie
.split(';')
.map( v => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
// session
const session = {}
http.createServer( async (req, res) => {
const cookies = parseCookies(req.headers.cookie);
if (req.url.startsWith('/login')) {
const url = new URL(req.url, 'http://localhost:8085');
const name = url.searchParams.get('name');
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/html; charset=utf-8' });
res.end(`${session[cookie.session].name님 안녕하세요`};
}else {
try{
const data = await fs.readFile(path.join(__dirname, '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(8085, () => {
console.log('8085번 포트에서 대기 중입니다!');
});
쿠키에 이름을 담아서 보내는 대신 uniqueInt라는 숫자 값을 보낸다.
사용자의 이름과 만료 시간은 uniqueInt 속성명 아래에 있는 session이라는 객체에 대신 저장한다.
사용자는 세션 아이디로만 소통한다.
실제로는 서버가 멈추거나 재시작되면 메모리에 저장된 변수가 초기화되는 문제 때문에 이런 방식을 사용하지는 않는다.
보통은 레디스나 멤캐시드같은 데이터베이스에 넣어둔다.