HTTP 프로토콜은 클라이언트와 한번 통신을 하고 나서 계속 연결을 유지하는게 아니라 요청과 응답이 끝나면 연결을 끊어버리는 특성이 있다. 또한 클라이언트와 서버의 통신이 끝나면 그 상태를(ex: 로그인) 유지하지 않는다.
그러한 특성때문에 사용자는 어떤 페이지를 들어가도 매번 로그인 인증을 해야할지도 모른다. 그럼 웹사이트에서 어떻게 매번 인증을 거치지 않고 로그인 상태를 유지할 수 있는걸까?? 그 방법은 쿠키와 세션을 사용하는 것이다.
쿠키는 서버가 사용자의 웹 브라우저에 전송하는 키와 값
으로 되어 있는 작은 데이터 파일이다
클라이언트에서 내용을 확인할 수 있고 다른 누군가가 값을 변경할 수도 있어서 보안에 취약하다.
따라서 쿠키는 보안이 필요가 없는 정보를 저장한다.
예를 들어 마음에 드는 옷을 장바구니에 담기 버튼을 클릭하면 서버 측에서 응답에 추가한 물품에 대한 정보를 담은 쿠키를 실어 보내준다. 그다음 통신이 있을 때마다 클라이언트에서 자동으로 요청의 헤더에 쿠키를 실어서 보내고, 서버는 그 쿠키 정보를 통해 사용자의 상태를 알 수 있는 것이다.
express를 사용해서 응답에 쿠키를 보내 볼 것이다.
npm i cookie-parser
로 cookie-parser
미들웨어를 사용하면 쉽게 쿠키를 다룰 수 있으며, req.cookies
로 cookie에 접근할 수 있다.
npm cookie-parser 사용법
const cookieParser = require('cookie-parser');
...
app.use(cookieParser('COOKIE_SECRET')); // 쿠키 secret을 사용할 수 있다.
view는 ejs로 만들고 부트스트랩을 사용했다.
여기서 이메일 저장을 체크하면 응답에 userEmail=입력한이메일
쿠키를 넣어 보내고, 새로고침하면 input에 email이 남아있게 구현해볼 것이다.
app.get('/', (req, res, next) => {
let user = {
email: "",
};
if (req.cookies) {
user.email = req.cookies.userEmail;
}
return res.render('index', {
user: user,
});
});
{
"email": "사용자가 입력한 이메일",
"password": "사용자가 입력한 비밀번호",
"checked": "이메일 저장 체크"
}
app.post('/login', (req, res, next) => {
const { email, checked } = req.body;
if (checked) { // 이메일 저장
res.cookie("userEmail", email);
}
return res.status(201).send("쿠키 생성!");
});
쿠키는 res.cookie(key, value, options);
로 응답에 실어 보낼 수 있다. 브라우저에 쿠키가 저장되면 req.cookies
로 무슨 쿠키가 저장되어 있는지 확인할 수 있다.
이메일 저장을 체크하고 요청을 보냈더니 Application창에 쿠키가 생성되는 것을 확인할 수 있었다.
새로고침을 해도 이메일은 그대로 남아있다. '/'주소로 GET요청이 들어오면 쿠키가 있는지 검사하고, 쿠키에 저장된 이메일이 있으면 렌더링할때 user.email을 넣어 화면에 user.email을 표시하도록 하였기 때문이다.
<!--index.ejs-->
<div class="form-group col-md-6">
<label for="inputEmail4">Email</label>
<input type="email" class="form-control" id="user-email" value=<%=user.email%>>
</div>
res.cookie("userEmail", email, {
maxAge: 30000,
});
세션은 쿠키를 이용해서 쿠키에는 세션 ID를 부여하고, 사용자에 관한 정보는 서버에서 관리하는 것이다. 서버에서는 사용자를 구분하기 위해 세션 ID를 부여하고, 브라우저를 종료하면 인증상태가 사라진다.
세션은 서버에 저장되므로 사용자가 많아질 수록 서버 메모리에 무리를 줄 수 있으며, 서버와 통신해야 하므로 쿠키보다 속도가 느리다.
express-session
미들웨어를 사용해서 세션을 생성해보자.express-session
은 내부적으로 req
에 session
을 추가해준다.
const session = require('express-session');
const app = express();
app.use(session({
secret: 'SESSION_SECRET',
resave: false,
saveUninitialized: true,
});
session
미들웨어를 사용할때 옵션을 추가해줘야 한다.
위의 로그인 버튼을 눌렀을때 동작하는 코드를 아래와 같이 바꾼다.
const user = {
email: "abcd@naver.com",
password: "1234",
nick: "abcd",
};
app.post('/login', (req, res, next) => {
const { email, password, checked } = req.body;
if (email === user.email && password === user.password) {
req.session.isUser = true;
req.session.user_nick = user.nick;
return res.status(201).send("로그인 성공!");
} else {
return res.status(404).send("아이디, 비밀번호가 맞지 않습니다!");
}
});
요청의 email과 password가 위에 미리 선언한 user의 정보와 일치하면 req.session
객체에 로그인 정보를 넣어준다.
홈화면으로 갔을때 로그인 하지 않은 상태이면 로그인 화면을 띄어주고, 로그인 했으면(세션에 isUser, user_nick이 있으면) user_nick님 안녕하세요! 메시지를 띄울 것이다. 아래와 같이 ejs 파일을 수정해 주었다.
<div class="card">
<div class="card-body">
<% if (!isUser) { %>
<h3 class="card-title text-center">
Login </h3>
<form>
<div class="form-row">
<div class="form-group col-md-6">
<label for="inputEmail4">Email</label>
<input type="email" class="form-control" id="user-email" value=<%=user.email%>>
</div>
<div class="form-group col-md-6">
<label for="inputPassword4">Password</label>
<input type="password" class="form-control" id="user-password">
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="save-email">
<label class="form-check-label" for="exampleCheck1">이메일 저장</label>
</div>
</div>
<button type="button" id="login-btn" class="btn btn-primary float-right">login</button>
</form>
<% } else {%>
<h2 class="text-center"><%=user.nick %>님 안녕하세요!</h2>
<% } %>
</div>
</div>
// index.ejs script
xhr.onload = function () {
if (xhr.status === 201) {
console.log(xhr.responseText);
location.reload(); // 성공 메시지를 받으면 새로고침한다.
} else {
console.error(xhr.responseText);
}
};
메인화면을 띄어주는 코드도 수정한다. req.session에 isUser와 user_nick이 있는지 검사해서 로그인 여부를 판단하고 화면을 렌더링한다.
app.get('/', (req, res, next) => {
let data = {
user: {
email: "",
nick: "",
},
isUser: false,
};
if (req.session.isUser && req.session.user_nick) { // 로그인 했으면
data.user.nick = req.session.user_nick;
data.isUser = req.session.isUser;
}
res.render('index', data);
});
로그인에 성공하면 아래와 같은 화면이 렌더링 되는 것을 확인할 수 있다.
Session.destroy(callback)
함수를 사용해서 세션 삭제를 할 수 있다. express-session
공식 문서에 있는 대로 사용해 보았다.
// express-session
req.session.destroy(function(err) {
// cannot access session here
})
<% } else {%>
<h2 class="text-center"><%= user.nick %>님 안녕하세요!</h2>
<button type="button" id="logout-btn" class="btn btn-primary float-right">logout</button>
<% } %>
...
<script>
...
const logoutBtn = document.querySelector("#logout-btn");
if (logoutBtn) {
logoutBtn.addEventListener("click", () => {
console.log("로그아웃");
const xhr = new XMLHttpRequest();
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.responseText);
location.reload();
} else {
console.error(xhr.responseText);
}
};
xhr.open('GET', '/logout');
xhr.send();
});
}
</script>
req.session.destroy()
함수를 사용해 세션을 삭제한다.app.get('/logout', (req, res, next) => {
req.session.destroy((err) => {
if (err) {
return res.status(403).send("로그아웃 실패!");
}
return res.send("로그아웃 성공!");
});
});
로그아웃 버튼을 누르자 새로고침이 되면서 로그인하지 않은 화면이 떴다. (session이 없어짐)
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<title>CookieSession</title>
</head>
<body>
<h1 class="text-center mb-5 mt-3">Cookie & Session</h1>
<div class="row">
<div class="col"></div>
<div class="col-8">
<div class="card">
<div class="card-body">
<% if (!isUser) { %>
<h3 class="card-title text-center">
Login
</h3>
<form>
<div class="form-row">
<div class="form-group col-md-6">
<label for="inputEmail4">Email</label>
<input type="email" class="form-control" id="user-email" value=<%= user.email%>>
</div>
<div class="form-group col-md-6">
<label for="inputPassword4">Password</label>
<input type="password" class="form-control" id="user-password">
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="save-email">
<label class="form-check-label" for="exampleCheck1">이메일 저장</label>
</div>
</div>
<button type="button" id="login-btn" class="btn btn-primary float-right">login</button>
</form>
<% } else {%>
<h2 class="text-center"><%= user.nick %>님 안녕하세요!</h2>
<button type="button" id="logout-btn" class="btn btn-primary float-right">logout</button>
<% } %>
</div>
</div>
</div>
<div class="col"></div>
</div>
<script>
const loginBtn = document.querySelector("#login-btn");
const logoutBtn = document.querySelector("#logout-btn");
if (loginBtn) {
loginBtn.addEventListener("click", () => {
const email = document.querySelector("#user-email").value;
const password = document.querySelector("#user-password").value;
const checked = document.querySelector("#save-email").checked;
const xhr = new XMLHttpRequest();
xhr.onload = function() {
if (xhr.status === 201) {
console.log(xhr.responseText);
location.reload();
} else {
console.error(xhr.responseText);
}
};
xhr.open('POST', '/login');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
email: email,
password: password,
checked: checked
}));
});
}
if (logoutBtn) {
logoutBtn.addEventListener("click", () => {
console.log("로그아웃");
const xhr = new XMLHttpRequest();
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.responseText);
location.reload();
} else {
console.error(xhr.responseText);
}
};
xhr.open('GET', '/logout');
xhr.send();
});
}
</script>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
</body>
</html>
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
// req.cookie를 만들어줌
app.use(cookieParser());
// req.session를 만들어줌
app.use(session({
secret: 'SESSION_SECRET',
resave: false,
saveUninitialized: true,
}));
app.get('/', (req, res, next) => {
let data = {
user: {
email: "",
nick: "",
},
isUser: false,
};
if (req.session.isUser && req.session.user_nick) {
data.user.nick = req.session.user_nick;
data.isUser = req.session.isUser;
}
res.render('index', data);
});
const user = {
email: "abcd@naver.com",
password: "1234",
nick: "abcd",
};
app.post('/login', (req, res, next) => {
const {
email,
password,
checked
} = req.body;
if (email === user.email && password === user.password) {
req.session.isUser = true;
req.session.user_nick = user.nick;
return res.status(201).send("로그인 성공!");
} else {
return res.status(404).send("아이디, 비밀번호가 맞지 않습니다!");
}
});
app.get('/logout', (req, res, next) => {
req.session.destroy((err) => {
if (err) {
return res.status(403).send("로그아웃 실패!");
}
return res.send("로그아웃 성공!");
});
});
app.listen('8081', () => {
console.log('8081번 포트에서 서버 실행 중!');
});
물론 실제 로그인을 할때 이렇게 구현하지 않는다.
실제 로그인 할때는 어떻게 구현하나요?