쿠키와 세션

클로이·2020년 12월 4일
0

HTTP 프로토콜은 클라이언트와 한번 통신을 하고 나서 계속 연결을 유지하는게 아니라 요청과 응답이 끝나면 연결을 끊어버리는 특성이 있다. 또한 클라이언트와 서버의 통신이 끝나면 그 상태를(ex: 로그인) 유지하지 않는다.
그러한 특성때문에 사용자는 어떤 페이지를 들어가도 매번 로그인 인증을 해야할지도 모른다. 그럼 웹사이트에서 어떻게 매번 인증을 거치지 않고 로그인 상태를 유지할 수 있는걸까?? 그 방법은 쿠키와 세션을 사용하는 것이다.

🍪쿠키란?

쿠키는 서버가 사용자의 웹 브라우저에 전송하는 키와 값으로 되어 있는 작은 데이터 파일이다
클라이언트에서 내용을 확인할 수 있고 다른 누군가가 값을 변경할 수도 있어서 보안에 취약하다.
따라서 쿠키는 보안이 필요가 없는 정보를 저장한다.

  • 장바구니에 담은 물품, 로그인 할때 아이디 저장 등
  • 사용자의 상태를 기억하기 위해 사용한다.
  • 쿠키는 유효시간이 지나기 전까지 그 브라우저를 종료하고 다시 들어와도 없어지지 않는다.
  • 클라이언트에 300개까지 쿠키를 저장할 수 있으며 하나의 도메인당 20개까지만 가질 수 있다.
  • 브라우저에 저장된다.

예를 들어 마음에 드는 옷을 장바구니에 담기 버튼을 클릭하면 서버 측에서 응답에 추가한 물품에 대한 정보를 담은 쿠키를 실어 보내준다. 그다음 통신이 있을 때마다 클라이언트에서 자동으로 요청의 헤더에 쿠키를 실어서 보내고, 서버는 그 쿠키 정보를 통해 사용자의 상태를 알 수 있는 것이다.

쿠키의 구성요소

  • 키: 각각의 쿠키를 구분하는데 사용
  • 값: 키와 관련된 값
  • 유효시간: 쿠키는 유효시간이 지나면 사라진다.
  • 도메인: 쿠키를 전송할 도메인
  • 경로: 쿠키를 전송할 요청 경로

응답에 쿠키 보내기

express를 사용해서 응답에 쿠키를 보내 볼 것이다.
npm i cookie-parsercookie-parser미들웨어를 사용하면 쉽게 쿠키를 다룰 수 있으며, req.cookies로 cookie에 접근할 수 있다.
npm cookie-parser 사용법

const cookieParser = require('cookie-parser');
...
app.use(cookieParser('COOKIE_SECRET'));	// 쿠키 secret을 사용할 수 있다.


view는 ejs로 만들고 부트스트랩을 사용했다.

여기서 이메일 저장을 체크하면 응답에 userEmail=입력한이메일쿠키를 넣어 보내고, 새로고침하면 input에 email이 남아있게 구현해볼 것이다.

  • 요청(GET) '/'
  • 응답(GET)
app.get('/', (req, res, next) => {
    let user = {
        email: "",
    };
    if (req.cookies) { 
       user.email = req.cookies.userEmail;
    }
    return res.render('index', {
        user: user,
    });
});
  • 요청(POST) '/login'
{
  "email": "사용자가 입력한 이메일",
  "password": "사용자가 입력한 비밀번호",
  "checked": "이메일 저장 체크"
}
  • 응답(POST)
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>
  • 쿠키 만료 시간 설정하기
    maxAge 옵션으로 만료 시간을 설정할 수 있다. 현재 시간으로부터 밀리초 단위로 만료 시간을 설정할 수 있다.
 res.cookie("userEmail", email, {
 	maxAge: 30000,
 });

📥세션

세션은 쿠키를 이용해서 쿠키에는 세션 ID를 부여하고, 사용자에 관한 정보는 서버에서 관리하는 것이다. 서버에서는 사용자를 구분하기 위해 세션 ID를 부여하고, 브라우저를 종료하면 인증상태가 사라진다.

  • 쿠키보다 보안이 뛰어나다.(서버에 저장하기 때문이다)
  • 보안상 중요한 작업일때 사용(로그인 한 상태인지 확인)
  • 서버에 저장된다. (DB에 저장해 사용한다)
  • 브라우저가 종료되면 사라진다. (만료시간과 상관없다)

세션은 서버에 저장되므로 사용자가 많아질 수록 서버 메모리에 무리를 줄 수 있으며, 서버와 통신해야 하므로 쿠키보다 속도가 느리다.

세션으로 로그인 구현해보기

express-session 미들웨어를 사용해서 세션을 생성해보자.express-session은 내부적으로 reqsession을 추가해준다.

const session = require('express-session');

const app = express();

app.use(session({
	secret: 'SESSION_SECRET',
  	resave: false,
  	saveUninitialized: true,
});

session 미들웨어를 사용할때 옵션을 추가해줘야 한다.

  • secret: 암호화를 위한 문자열이다. 외부에 노출되면 안되며 필수 옵션이다. (코드에 그대로 넣지 않고 .env등을 사용한다.)
  • resave
  • saveUninitialized

위의 로그인 버튼을 눌렀을때 동작하는 코드를 아래와 같이 바꾼다.

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
})
  1. 로그아웃 버튼을 추가하고 '/logout' 주소로 GET요청이 가도록 한다.
<% } 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>
  1. 서버에서 '/logout' 주소로 GET요청이 오면 req.session.destroy()함수를 사용해 세션을 삭제한다.
app.get('/logout', (req, res, next) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(403).send("로그아웃 실패!");
    }
    return res.send("로그아웃 성공!");
  });
});


로그아웃 버튼을 누르자 새로고침이 되면서 로그인하지 않은 화면이 떴다. (session이 없어짐)

전체 코드

  • index.ejs
<!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>
  • app.js (서버)
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번 포트에서 서버 실행 중!');
});

물론 실제 로그인을 할때 이렇게 구현하지 않는다.

참고

Network 쿠키와 세션 개념 by 라이언곰돌이푸
생활코딩 express-session을 이용한 인증구현

profile
개발자가 되고싶어요

2개의 댓글

comment-user-thumbnail
2021년 2월 17일

실제 로그인 할때는 어떻게 구현하나요?

1개의 답글