IT/웹

[Node.js와 Session] Session을 이용한 인증 구현 (생활코딩 강의 기반)

kykyky 2024. 2. 1. 01:04

✅ 이 실습을 통해 우리가 만드려는 것은

 

Session 방식을 이용해 로그인과 회원가입 등 인증 기능이 있는 웹사이트입니다. 

사용자가 브라우저를 통해 서버에 접속하면서, 브라우저는 쿠키를 서버로 전송한다. 
서버 컴퓨터에는 이러한 쿠키들이 session으로서 저장되고,
사용자에게는 단지 식별자 (= Session ID) 쿠키만이 저장된다.

 

 

Q. 왜 Cookie가 아니라 Session을 사용하나?

A. Cookie를 통한 인증은, 가능은 하지만, 보안 상의 문제가 크다. (클라이언트 쪽에 ID와 PW 쿠키가 있으므로)

 

 

 

✅ 디렉토리 구성과 주요 코드 설명

 

 

// 주석은 크게 나뉘어진 단계이고,
//// 주석은 해당 코드에 대한 자세한 설명입니다.

 

 

authFunctions.js

module.exports = {
  // 세션 로그인이 되어있으면 true, 아니면 false를 return하는 함수

  isOwner: function (request, response) {
    if (request.session.isLogined) {
      return true; 
    } 
    
    else {
      return false;
    }
  },
  
  // 세션 로그인 여부에 따라 다른 UI를 return하는 함수

  getAuthStatusUI: function (request, response) {
    var authStatusUI = 'Log in first!' //// 로그인 안되어있는 경우

    if (this.isOwner(request, response)) {
      authStatusUI = `Welcome, ${request.session.nickname}! | <a href="/auth/logout">Logout</a>`; //// 로그인 되어있는 경우
    }

    return authStatusUI;
  }
}

 

 

authRouter.js

var express = require('express');
var router = express.Router();
var template = require('./template.js');
var db = require('./db.js');
const bcrypt = require("bcrypt");

// 로그인 페이지

router.get('/login', function (request, response) {
    var title = 'Login';
    var html = template.HTML(
            title,
            `
            <h2>Login</h2>
            <form action="/auth/login_process" method="post">
            <p><input class="login" type="text" name="id" placeholder="ID"></p>
            <p><input class="login" type="password" name="pwd" placeholder="Password"></p>
            <p><input class="btn" type="submit" value="Login"></p>
            </form>            
            <p>Don't have an account?  <a href="/auth/register">Register</a></p> 
            `, 
            ''
            );
    response.send(html);
});

// 로그인 버튼을 누른 순간

router.post('/login_process', function (request, response) {
    var id = request.body.id;
    var password = request.body.pwd;

    // ID와 PW 모두 입력된 경우

    if (id && password) { 
        
        db.query('SELECT id, password FROM userTable WHERE id = ?', [id], function(error, results, fields) { //// 입력된 id를 가진 row의 id, password를 SELECT 
            // 에러 처리
            
            if (error) throw error;

            // 입력한 ID가 DB에 존재하는 경우

            if (results.length > 0) { 
                password_e_indb = results[0].password;
                const isSame =  bcrypt.compareSync(password, password_e_indb);

                // 입력한 PW가 올바른 경우

                if (isSame) {
                    request.session.isLogined = true; //// 세션 정보 업데이트
                    request.session.nickname = id;
                    request.session.save(function () {
                        response.redirect(`/`); //// root 페이지로 리다이렉트
                    })}

                // 입력한 PW가 올바르지 않은 경우

                else {              
                    response.send(`<script type="text/javascript">alert("Login information (PW) does not match."); 
                    document.location.href="/auth/login";</script>`);    
                }        
            } 

            // 입력한 ID가 DB에 존재하지 않는 경우
            
            else {              
                response.send(`<script type="text/javascript">alert("Login information (ID) does not match."); 
                document.location.href="/auth/login";</script>`);    
            }            
        });
    } 
    
    // ID나 PW 중 입력되지 않은 값이 있는 경우
    
    else {
        response.send(`<script type="text/javascript">alert("Enter your ID and Password!"); 
        document.location.href="/auth/login";</script>`);    
    }
});

// 로그아웃 페이지

router.get('/logout', function (request, response) {
    request.session.destroy(function (err) { //// 세션을 삭제
        response.redirect('/');
    });
});

// 회원가입 페이지

router.get('/register', function(request, response) {
    var title = 'Register';    
    var html = template.HTML(
        title, 
        `
        <h2>Register</h2>
        <form action="/auth/register_process" method="post">
        <p><input class="login" type="text" name="id" placeholder="ID"></p>
        <p><input class="login" type="password" name="pwd" placeholder="Password"></p>    
        <p><input class="login" type="password" name="pwd2" placeholder="Enter Password Again"></p>
        <p><input class="btn" type="submit" value="Create"></p>
        </form>            
        <p><a href="/auth/login">Back to the Login Page</a></p>
        `, 
        ''
        );
    response.send(html);
});
 
// 회원가입 버튼을 누른 순간

router.post('/register_process', function(request, response) {    
    var id = request.body.id;
    var password = request.body.pwd;
    var password2 = request.body.pwd2;

    const password_e = bcrypt.hashSync(password, 10); //// 비밀번호를 암호화함. saltOrRounds: salt를 몇 번 돌릴건지.

    // 모든 게 입력된 경우

    if (id && password && password2) {
        
        db.query('SELECT * FROM userTable WHERE id = ?', [id], function(error, results, fields) { //// 입력한 ID를 가진 row를 SELECT
            // 에러 처리 
            
            if (error) throw error;

            // 입력된 ID가 DB에 아직 없고, PW = PW2인 경우 (정상)

            if (results.length <= 0 && password == password2) { 
                db.query('INSERT INTO usertable (id, password) VALUES(?,?)', [id, password_e], function (error, data) { //// 입력한 ID와 암호화된 PW를 테이블에 INSERT
                    if (error) throw error;
                    response.send(`<script type="text/javascript">alert("Registration is Complete!");
                    document.location.href="/";</script>`);
                });
            } 

            // PW != PW2인 경우

            else if (password != password2) { 
                response.send(`<script type="text/javascript">alert("The entered passwords are not same."); 
                document.location.href="/auth/register";</script>`);    
            }

            // 입력된 ID가 DB에 이미 있는 경우

            else { 
                response.send(`<script type="text/javascript">alert("The ID already exists."); 
                document.location.href="/auth/register";</script>`);    
            }            
        });
    } 
    
    // 아직 입력되지 않은 것이 있는 경우

    else { 
        response.send(`<script type="text/javascript">alert("There is information that has not been entered."); 
        document.location.href="/auth/register";</script>`);
    }
});

// export

module.exports = router;

 

 

db.js

var mysql = require('mysql2');

var db = mysql.createConnection({
    host: 'localhost',
    user: 'root', //// 여러분이 설정하신 사용자 이름으로 해주셔야 합니다
    password: '0000', //// 여러분이 설정하신 비밀번호로 해주셔야 합니다
    database: 'mydb1' //// 우리가 생성해준 database의 이름과 동일해야 합니다
});

db.connect();

module.exports = db;

 

 

template.js

module.exports = {
    HTML: function (title, content, authStatusUI) {
      return `
      <!doctype html>
      <html>
      <head>    
        <title>Auth by Session in Node.js - ${title}</title>
        <meta charset="utf-8">
      </head>
      <body>
        ${authStatusUI}
        ${content}
      </body>
      </html>
      `;
    }
  }

 

 

main.js

const express = require('express')
const app = express()

const session = require('express-session')
const FileStore = require('session-file-store')(session)
app.use(session({
  secret: '~~~',	//// 원하는 문자 입력
  secure: true, //// https 통신만을 허용함
  HttpOnly: true, //// 자바스크립트를 통한 탈취 방지
  resave: false,
  saveUninitialized: true,
  store: new FileStore(), //// 세션 데이터를 서버 컴퓨터에 저장함. 여기를 mySQL로 설정하면 mySQL을 사용할 수도 있음.
}))

const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }));

var authFunctions = require('./lib/authFunctions.js');
var template = require('./lib/template.js');

// "/"로 접속된 경우

app.get('/', (req, res) => {
  // 로그인 안되어있는 경우 -> 로그인부터 하라고 리다이렉트

  if (!authFunctions.isOwner(req, res)) {  
    res.redirect('/auth/login');

    return false;
  } 
  
  // 로그인 되어있는 경우

  else {                                      
    res.redirect('/main');
    
    return false;
  }
})

// "/auth"로 접속된 경우 

var authRouter = require('./lib/authRouter.js');
app.use('/auth', authRouter); 

// "/main"로 접속된 경우

app.get('/main', (req, res) => {
  // 로그인 안되어있는 경우 -> 로그인부터 하라고 리다이렉트

  if (!authFunctions.isOwner(req, res)) {  
    res.redirect('/auth/login');

    return false;
  }

  // 로그인 되어있는 경우

  var html = template.HTML(
    'Welcome',
    `<hr>
        <h2>Welcome to Main page!</h2>
    `,
    authFunctions.getAuthStatusUI(req, res)
  );
  res.send(html);
})

// 서버가 클라이언트의 request를 listen

app.listen(3000, () => {
  console.log('Example is listening on port 3000!')
})

 

 

 

 

✅ mySQL에서 사용자 정보를 담을 table 만들기

 

1. 먼저 new connection을 만들어준 뒤 비밀번호를 설정합니다. 

 

 

 

2. Query에 아래와 같이 입력하고 이 코드만 실행하여 우선 database를 만들고, 왼쪽의 SCHEMAS 목록을 새로고침하여, 방금 만든 database를 선택해줍니다. 

CREATE DATABASE mydb1;

 

 

 

3. Query에 아래와 같이 입력하고 이 코드만 실행하여 database 내의 table을 완성하면 됩니다.

CREATE TABLE userTable (
  id varchar(50) NOT NULL,
  password varchar(255) NOT NULL,
  PRIMARY KEY(id)
) charset=utf8;

 

 

 

 

✅ 인증 기능 사용해보기

 

1. 서버 열기

 

우리의 작업 디렉토리 위치에서 커맨드창에

node main.js

를 입력합니다.

 

 

 

2. 웹사이트 최초 접속

 

처음으로 localhost:3000으로 접속하면 우선 로그인 페이지가 뜹니다.

 

 

이때 쿠키를 확인해보면 Session ID 값을 볼 수 있습니다.

 

 

이 Session에 대한 정보들은 서버 컴퓨터의 Sessions 폴더 내에 파일로서 저장됩니다. 

이때 이 파일의 이름이 위의 Session ID와 동일하단 것을 알 수 있습니다!! (앞의 몇 글자만 제외하면)

 

 

 

3. 회원가입

 

당연히 아직 사용자 정보가 없을테니, 회원가입 링크를 눌러 회원가입 페이지로 간 후 정보를 입력한 뒤 Create 버튼을 누르면 회원가입이 되며 다시 로그인 페이지로 리다이렉션 됩니다. 

(비밀번호 재입력을 다르게 입력했거나, 이미 존재하는 ID인 경우 오류 메시지가 뜹니다.)

 

 

 

 

 

mySQL의 Query에 아래와 같이 입력하고 사용자 정보 table을 다시 봅시다. 방금 회원가입을 통해 사용자 정보가 table에 저장되었습니다.

이때 비밀번호는 날 것의 평문 상태가 아니라 암호화되어 저장됨으로써, 보안 상의 문제를 방지합니다.

SELECT * from userTable;

 

 

 

4. 로그인

 

로그인 페이지에서 방금 입력했던 ID와 PW를 입력하면 로그인이 성공적으로 완료되며 메인 페이지로 리다이렉션 됩니다.

(사용자 정보를 잘못 입력할 경우 오류 메시지가 뜹니다.)

 

 

 

이때 다시 Sessions 폴더의 그 파일을 다시 보면, isLogined가 true로 설정되고, nickname이 추가된 것을 확인할 수 있습니다. 로그인에 성공했기 때문입니다.

 

 

동일한 브라우저에서 새 창을 띄운 경우, Session ID가 동일하고, 로그인 상태가 유지되어 있습니다.