로그인을 하고 난 후, 컴퓨터를 껏다 켜도 로그인이 유지되어있는 경우가 있다. 이는 클라이언트가 서버에 접속자가 누구인지 알려주고 있기 때문이다.
서버는 접속자가 누구인지 기억하기 위해서 응답에 "쿠키" 라는 것을 같이 보낸다. 쿠키는 유효기간이있으며, "key-value" 형태의 쌍을 가지고 있다. 브라우저는 서버로 부터 온 쿠키를 저장한 후, 요청할 때 쿠키를 함께 보내고, 서버는 이 쿠키를 읽어 사용자를 식별하게 되는것이다.
쿠키는 브라우저에서 접하기 쉽다. 개발자 도구를 들어간 뒤 Application창에서 브라우저의 쿠키들을 볼 수 있다.
기본적인 쿠키 방식 코드를 작성해 보자. 쿠키는 요청의 header에 담겨져서 보내며, 헤더의 "Set-Cookie" 필드에 쿠키를 저장한다.
const http = require('http')
http.createServer((req,res) => {
console.log(req.url, req.headers.cookie)
res.writeHead(200,{
'Content-Type' : 'text/html',
'Set-Cookie' : 'exampleCookie=test'
})
res.end("<h2>Cookie Example</h2>")
}).listen(8000, () => {
console.log("Listening on port 8000")
})
req.headers.cookie에서 요청의 헤더에 담긴 쿠키를 가져올 수 있다. 그리고 writeHead에서 Header 부분에 Set-Cookie로 쿠키를 추가하는것도 볼 수 있다. localhost:8000에 접속 후 개발자 도구로 살펴보자
브라우저 쿠키 부분과 응답 헤더에 쿠키가 들어가있는것을 볼 수 있다. 이번에는 쿠키를 이용해 사용자를 식별하는 코드를 작성해 보자.
<!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>Cookie & Session</title>
</head>
<body>
<form action="/login">
<input id="name" name="name" 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 parseCookie = (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 = parseCookie(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.toUTCString()};HttpOnly;Path=/`
})
res.end()
}
// 만약 name이라는 쿠키가 존재한다면
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('./test.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(8004,() => {
console.log('Listening on port 8004')
})
localhost:8004로 들어가게 되면 아래와 같이 html form 하나가 나오는것을 볼 수 있다.
여기에 아무 이름을 작성하고 로그인 버튼을 눌러보자. 개발자 도구로 들어가 쿠키를 확인하면 아래와 같이 쿠키 정보가 브라우저에 저장된것을 볼 수 있다.
이번에는 Network부분으로 들어가 보자. 네트워크 부분에 /login?name=(작성한 이름)
형태의 쿼리가 있는것을 볼 수 있다. 기본적으로 button을 통해서 /login이라는 endpoint에 요청을 보냈으며, 메소드 지정을 해주지 않았기 때문에 querystring 형식으로 전달되는것을 알 수 있다. 현재 localhost가 열려있는 창을 껏다가 다시 켜도 login페이지가 아닌, 인사창이 그대로 나오는것을 알 수 있다.
res.writeHead(302,{
Location: '/',
'Set-Cookie' : `name=${encodeURIComponent(name)};Expires=${expires.toUTCString()};HttpOnly;Path=/`
})
'Set-Cookie'필드의 value 필드들을 살펴보자.
여기서는 login을 GET 요청으로 querystring에 전달하는 방식으로 작성하였지만, 실제 로그인할때는 POST방식으로 동작하게 된다. 위 예제를 응용해서 POST로 요청하는 경우를 구현해 보았다.
const http = require('http')
const fs = require('fs').promises
const url = require('url')
const qs = require('querystring')
// 문자열로된 쿠키값을 자바스크립트 객체로 바꾸는 함수
const parseCookie = (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 = parseCookie(req.headers.cookie)
if(req.method.toLocaleLowerCase() === "post"){
if(req.url.startsWith("/login")){
console.log("here")
let body = ''
req.on('data',(data) => {
body += data;
})
req.on('end', () => {
let expires = new Date()
const [ key,value ] = body.split("=")
body = [key,value.replace("+"," ")].join('=')
expires.setMinutes(expires.getMinutes() + 5)
console.log(body)
res.writeHead(302,{
Location:'/',
'Set-Cookie' : `${body}; Expires=${expires.toUTCString()}; HttpOnly; Path=/`
})
return res.end()
})
}
}
else if(req.method.toLowerCase() === "get"){
if(cookies.name){
res.writeHead(200, {
'Content-Type' : 'text/html;charset=utf-8'
})
return res.end(`<h2>${cookies.name}님 안녕하세요</h2>`)
}
else{
try{
const data = await fs.readFile('./test.html')
res.writeHead(200, {
'Content-Type' : 'text/html;charset=utf-8'
})
return res.end(data)
}catch(err){
console.error(err)
res.writeHead(500,{
'Content-Type' : 'text/html;charset=utf-8'
})
return res.end("<p>Server Error</p>")
}
}
}
}).listen(8004,() => {
console.log('Listening on port 8004')
})
쿠키는 한계점이 있다. 아래 사진을 보면 알 수 있듯이 쿠키값을 직접 브라우저에 전달하게 되면, 악의 적인 사용자에 의해 쿠키를 탈취당하거나 쿠키가 조작될 수 있다.
Cookie의 한계를 도와주는 것이 Session방식이다.서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디를 통해서 소통한다.그리고 세션을 위해 사용하는 쿠키를 세션 쿠키 라고 부른다. 아래 예제에서는 session이라는 변수를 통해 Session을 유사 사용해보겠지만, 실제 서비스에서는 Redis 혹은 Memcached같은 In-memory DB에 넣어둔다.
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)
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.toUTCString()};HttpOnly;Path=/`
})
return res.end()
}else if(cookies.session && session[cookies.session].expires > new Date()){
res.writeHead(200,{'Content-Type' : 'text/html;charset=utf-8'})
return res.end(`<h2>${session[cookies.session].name}님 안녕하세요</h2>`)
}else{
try{
const data = await fs.readFile('./test.html')
res.writeHead(200,{'Content-Type' : 'text/html;charset=utf-8'})
return res.end(data)
}catch(err){
res.writeHead(500,{'Content-Type' : 'text/plain;charset=utf-8'})
res.end(err.messages)
}
}
}).listen(8005,() => {
console.log('Listening on port 8005')
})