상세 컨텐츠

본문 제목

NodeJS 에서 세션기능 구현하기~!

Computer Science/Security

by Yongari 2023. 1. 3. 15:51

본문

 

 

 

 

배우기 전에 알아야할 것

  • 쿠키에 대한 이해 

 

성취 목표

  • 세션의 개념을 이해하기
  • 쿠키와 세션은 서로 어떤 관계고 각각이 인증에 있어서 어떤 목적으로 존재하는지 이해할 수 있다.
  • 세션의 한계를 이해할 수 있다.

 

 

1.환경변수 설정

DB 유저이름=""

DB 패스워드=""

Database 이름=""

 

 

 

2. 데이터베이스 마이그레이션

이번 스프린트에서 시퀄라이즈(Sequelize)를 사용해 데이터베이스를 조작합니다.

시퀄라이즈는 데이터베이스와 자바스크립트 코드로 작성된 서버의 데이터를 호환시켜주는 ORM 툴입니다.

시퀄라이즈와 같은 ORM을 사용해 코드 상에서 객체 형태로 되어있는 데이터와 데이터 베이스에 저장된 데이터의 타입을 호환시킬 수 있습니다. 

 

ORM - 포스팅 정리 

 

여기서는 시퀄라이즈를 사용해 데이터베이스 마이그레이션을 진행합니다. 데이베이스 마이그레이션(database migration)은 시퀄라이즈와 같은 라이브러리를 사용해 작성된 데이터베이스 스키마를 데이터베이스에 주입하는 것을 의미합니다.

 

 

설정확인


1. config.js 확인 (db접속정보,아이피 등 확인)

const dotenv = require('dotenv');
dotenv.config();

module.exports = {
  development: {
    username: 'root',
    password: process.env.DATABASE_PASSWORD,
    database: 'authentication',
    host: '127.0.0.1',
    dialect: 'mysql',
    logging: false
  },
  test: {
    username: 'root',
    password: process.env.DATABASE_PASSWORD,
    database: 'authentication',
    host: '127.0.0.1',
    dialect: 'mysql',
  },
  production: {
    username: 'root',
    password: process.env.DATABASE_PASSWORD,
    database: 'authentication',
    host: '127.0.0.1',
    dialect: 'mysql',
  },
};

2. 시퀄라이즈 마이그레이션 단계


npm install >> package.json에 작성된 모듈들 설치

npx sequelize-cli db:migrate >> sequeliz-cli를 사용해 데이터베이스 마이그레이션 진행
다음과 같이 테이블을 만들고 지울 수 있는 기능으로 보인다.

'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER,
      },userId: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Users');
  },
};




3. npx sequelize-cli db:seed:all 

다음 코드와 같이 데이터를 insert할 때 사용하는 것으로 보인다.

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    /**
     * 시쿼라이즈가 실행할 시드 명령어를 작성합니다..
     *
     * 예시:
     * await queryInterface.bulkInsert('People', [{
     *   name: 'John Doe',
     *   isBetaMember: false
     * }], {});
     */
    return queryInterface.bulkInsert('Users', [
      {
        id: '0',
        userId: 'kimcoding',
        password: '1234',
        email: 'kimcoding@authstates.com',
        createdAt: new Date(),
        updatedAt: new Date(),
      },
    ]);
  },

  down: async (queryInterface, Sequelize) => {
    /**
     * 시드를 취소하기 위한 명령어를 작성합니다.
     *
     * 예시:
     * await queryInterface.bulkDelete('People', null, {});
     */
    return queryInterface.bulkDelete('Users', null, {});
  },
};

 

 

서버 구현하기

 

 

index.js

  • index.js에서 cors 및 세션 옵셜 설정하기
  • http 인증서 준비하기 -> mkcert로 생성했던 key.pem, cert.pem
const express = require("express");
const cors = require("cors");
const session = require("express-session");
const logger = require("morgan");
const fs = require("fs");
const https = require("https");
const usersRouter = require("./routes/user");

const app = express();

const PORT = process.env.PORT || 4000;

// TODO: express-session 라이브러리를 이용해 쿠키 설정을 해줄 수 있습니다.
app.use(
  session({
    secret: "@codestates",
    resave: false,
    saveUninitialized: true,
    cookie: {
      domain: "localhost",
      path: "/",
      maxAge: 24 * 6 * 60 * 10000,
      //Cookie의 SameSite 속성의 기본 값이 "None" 항상 쿠키를 보내줄 수 있음, 다만 쿠키옵션중 Secure옵션이 필요함
      sameSite: "none",
      // 자바스크립트로 쿠키 접근 못하게하려면 true 설정
      httpOnly: true,
      // 쿠키 secure 옵션
      secure: true,
    },
  })
);
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// TODO: CORS 설정이 필요합니다. 클라이언트가 어떤 origin인지에 따라 달리 설정할 수 있습니다.
// 메서드는 GET, POST, OPTIONS를 허용합니다.
app.use(cors());
/**
 * /users 요청에 대해서 라우터를 이용하기 때문에,
 * 반드시 아래와 같은 주소와 메서드로 요청을 보내야 합니다.
 *
 * POST https://localhost:4000/users/login,
 * POST https://localhost:4000/users/logout,
 * GET https://localhost:4000/users/userinfo
 */
app.use("/users", usersRouter);

let server;

// 인증서 파일들이 존재하는 경우에만 https 프로토콜을 사용하는 서버를 실행합니다.
// 만약 인증서 파일이 존재하지 않는경우, http 프로토콜을 사용하는 서버를 실행합니다.
// 파일 존재여부를 확인하는 폴더는 서버 폴더의 package.json이 위치한 곳입니다.
if (fs.existsSync("./key.pem") && fs.existsSync("./cert.pem")) {
  server = https
    .createServer(
      {
        key: fs.readFileSync(__dirname + `/` + "key.pem", "utf-8"),
        cert: fs.readFileSync(__dirname + `/` + "cert.pem", "utf-8"),
      },
      app
    )
    .listen(PORT);
} else {
  server = app.listen(PORT);
}
module.exports = server;



controller/login.js (POST /users/login)

1. request로부터 받은 userId와 password와 일치하는 유저가 db에 있는지 확인

2. 일치하는 유저가 없을 경우 > 로그인 요청 거절

3. 일치하는 유저가 있을 경우 > 세션에 userId를 저장 

 

const { Users } = require("../../models");

module.exports = {
  post: async (req, res) => {
    // userInfo는 유저정보가 데이터베이스에 존재하고, 완벽히 일치하는 경우에만 데이터가 존재합니다.
    // 만약 userInfo가 NULL 혹은 빈 객체라면 전달받은 유저정보가 데이터베이스에 존재하는지 확인해 보세요
    const userInfo = await Users.findOne({
      where: { userId: req.body.userId, password: req.body.password },
    });

    // 결과가 존재하는 경우 세션 객체에 userId가 저장되어야 합니다.
    // userInfo에 정보가 없으면 404 코드를 반환하고 not authorized 반환
    if (!userInfo) {
      return res.status(404).send({ message: "not authorized" });
    } else {
      //그외에 userInfo가 있으면 다음과 같이 세션을 저장한다.
      // 이후 data에는 userInfo를 담고 message에는 ok를 담는다.
      req.session.save(function () {
        req.session.userId = userInfo.userId;
        return res.json({ data: userInfo, message: "ok" });
      });
    }
  },
};

 

 

 

controller/logout.js

1. 세션 객체에 저장한 값이  존재하면 세션을 삭제하기 ( 자동으로 클라이언트 쿠키는 갱신된다.)

 

module.exports = {
  post: (req, res) => {
    // TODO: 세션 아이디를 통해 고유한 세션 객체에 접근할 수 있습니다.
    // 앞서 로그인시 세션 객체에 저장했던 값이 존재할 경우, 이미 로그인한 상태로 판단할 수 있습니다.
    // 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요.

    if (!req.session.userId) {
      res.status(400).send();
    } else {
      // your code here
      // TODO: 로그아웃 요청은 세션을 삭제하는 과정을 포함해야 합니다.
	
      //저장된 세션 삭제 
      req.session.destroy();
      //로그인 이후 로그아웃을 요청할 때 상태코드 200을 리턴해야함
      res.status(200).send();
    }
  },
};

 

 

 

controller/userinfo.js

1.세션 객체에 저장한 값이 존재하면 사용자 정보를 데이터베이스에서 조회한 후 응답으로 전달합니다. 

2.세션 객체에 저장한 값이 존재하지 않으면 요청을 거절합니다. 

const { Users } = require("../../models");

module.exports = {
  get: async (req, res) => {
    // console.log("req.session", req.session);
    // TODO: 세션 객체에 담긴 값의 존재 여부에 따라 응답을 구현하세요.
    // HINT: 세션 객체에 담긴 정보가 궁금하다면 req.session을 콘솔로 출력해보세요

    if (!req.session.userId) {
      res.status(400).send({ message: "not authorized" });
    } else {
      //옵션1 유저정보를 찾아서 같이 반환
      const result = await Users.findOne({
        where: { userId: req.session.userId },
      });
      res.status(200).json({ data: result, message: "ok" });

      //옵션 2 상태코드와 메세지 ok만 반환 
      // res.status(200).send({ message: "ok" });
      // console.log("result", result);
      // TODO: 데이터베이스에서 로그인한 사용자의 정보를 조회한 후 응답합니다.
    }
  },
};

 

 

클라이언트 구현하기


controller/Mypage.js

import React from "react";
import axios from "axios";


function Mypage(props) {
  const handleLogout = () => {
    // TODO: 서버에 로그아웃 요청을 보낸다음 요청이 성공하면 props.logoutHandler를 호출하여 로그인 상태를 업데이트 해야 합니다.
    axios
      .post("https://localhost:4000/users/logout", {
        withCredentials: true,
      })
      .then(() => {
        props.logoutHandler();
      });
  };
  return props.userData == null ? (
    <div>Loading...</div>
  ) : (
    <div>
      <div className="mypageContainer">
        <div>
          <span className="title">Mypage</span>
          <button className="logoutBtn" onClick={handleLogout}>
            logout
          </button>
        </div>
        <hr />

        <div>
          안녕하세요. <span className="name">{props.userData.userId}</span>님!
          로그인이 완료되었습니다.
        </div>
        <br />
        <div className="item">나의 유저 네임: {props.userData.userId}</div>
        <div className="item">나의 이메일 주소: {props.userData.email}</div>
      </div>
    </div>
  );
}

export default Mypage;


controller/Login.js

import React, { Component } from "react";
import axios from "axios";

class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: "",
    };
    this.inputHandler = this.inputHandler.bind(this);
    this.loginRequestHandler = this.loginRequestHandler.bind(this);
  }

  inputHandler(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

  async loginRequestHandler() {
    // TODO: 로그인 요청을 보내세요.

    // 로그인에 성공하면
    // - props로 전달받은 함수를 호출해, 로그인 상태를 변경하세요.
    // - GET /users/userinfo 를 통해 사용자 정보를 요청하세요

    // 사용자 정보를 받아온 후
    // - props로 전달받은 함수를 호출해, 사용자 정보를 변경하세요.
    try {
      await axios({
        url: "https://localhost:4000/users/login", // 통신할 웹문서
        method: "post", // 통신할 방식
        headers: {
          accept: "application/json",
        },
        data: {
          // 인자로 보낼 데이터
          userId: `${this.state.username}`,
          password: `${this.state.password}`,
        },
        withCredentials: true,
      });
      this.props.loginHandler();
      const result = await axios({
        method: "get",
        url: "https://localhost:4000/users/userinfo",
        headers: {
          accept: "application/json",
        },
        withCredentials: true,
      });
      const { userId, email } = result.data.data;
      this.props.setUserInfo({
        userId,
        email,
      });
    } catch (err) {
      alert(err);
    }
  }

  render() {
    return (
      <div className="loginContainer">
        <div className="inputField">
          <div>Username</div>
          <input
            name="username"
            onChange={(e) => this.inputHandler(e)}
            value={this.state.username}
            type="text"
          />
        </div>
        <div className="inputField">
          <div>Password</div>
          <input
            name="password"
            onChange={(e) => this.inputHandler(e)}
            value={this.state.password}
            type="password"
          />
        </div>
        <div className="passwordField">
          <button onClick={this.loginRequestHandler} className="loginBtn">
            Login
          </button>
        </div>
      </div>
    );
  }
}

export default Login;

 

 

 

 

App.js

import React, { Component } from 'react';

import Login from './components/Login';
import Mypage from './components/Mypage';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isLogin: false,
      userData: null,
    };
    this.loginHandler = this.loginHandler.bind(this);
    this.logoutHandler = this.logoutHandler.bind(this);
    this.setUserInfo = this.setUserInfo.bind(this);
  }

  loginHandler() {
    this.setState({
      isLogin: true,
    });
  }

  setUserInfo(object) {
    this.setState({ userData: object });
  }

  logoutHandler() {
    this.setState({
      isLogin: false,
    });
  }

  render() {
    const { isLogin } = this.state;
    return (
      <div className='App'>
        {isLogin ? (
          <Mypage
            logoutHandler={this.logoutHandler}
            userData={this.state.userData}
          />
        ) : (
            <Login
              loginHandler={this.loginHandler}
              setUserInfo={this.setUserInfo}
            />
          )}
      </div>
    );
  }
}

export default App;

 

관련글 더보기