본문 바로가기
프로그래밍/웹 개발

[node.js + React] 쿠키와 세션을 활용해 로그인 기능 구현

by 제이콥J 2021. 9. 10.

코드스테이츠에서 세션 인증을 통해 로그인 기능을 구현하는 과제를 마쳤다.

진행 과정 및 코드는 다음과 같다.

 

<HTTPS 통신 순서>

1. 클라이언트의 login 페이지 : username, password 입력 후 로그인 시도 (post 요청)

 

2. 서버의 login 컨트롤러 : request body의 userId와 password 가 일치하는 유저 조회

  - 해당 유저가 없으면 실패 응답 보내기

  - 해당 유저가 있으면 Session에 userId 를 저장하고, 성공 응답 보내기

 

3. 클라이언트의 Login 페이지 : 서버의 성공 응답 받기

  - login 상태 변경 : 로그인에 성공했으므로 login 상태를 true로 바꾸기

  - 서버로 해당 유저의 정보를 요청하기 (get 요청)

 

4. 서버의 userinfo 컨트롤러 : 요청의 session에 userId가 있는지 확인하기

  - req.session에 userId가 없다면 실패 응답 보내기

  - 있다면 해당 유저 정보를 response 객체에 담아서 성공 응답으로 보내기

 

5. 클라이언트의 Login 페이지 : 응답으로 받은 유저 정보로 유저 상태를 변경하기→ Mypage 컴포넌트에 적용 

 

6. 클라이언트의 app.js : login 상태가 true로 변경되었으므로, Login 페이지를 Mypage 페이지로 변경하기

 

7. 클라이언트의 Mypage 페이지 : logout 버튼 누루기 (post 요청)

 

8. 서버의 logout 컨트롤러 : 요청의 session에 userId가 있는지 확인하기

  - req.session에 userId가 없다면, 실패 응답 보내기

  - 있다면 세션 정보를 삭제하고, 성공 응답 보내기

 

9. 클라이언트의 My page 페이지 : 로그아웃에 성공했다면 login 상태를 false로 바꾸기

 

10. 클라이언트의 app.js : login 상태가 false로 변경되었으므로, Mypage 페이지를 Login 페이지로 변경하기

 

<코드 구현 순서>

1. 서버 : user 모델 생성 후 login, logout, userinfo 컨트롤러 제작하기

2. 클라이언트 : app.js, Login & Mypage 컴포넌트 제작하기

 

* 공식문서 링크 : https://www.npmjs.com/package/express-session

 


User 모델 생성 및 미들웨어 설정

1. mkcert로 사설 인증서를 발급하여 package.json 이 있는 폴더에 옮기기

 

2. Sequelize User 모델 생성하기

 

3. .env 파일에 MySQL 접속 정보 입력하기

 

4. express를 통한 미들웨어 설정하기

  - express-session 라이브러리를 이용해 쿠키 설정

  - cors 설정 진행

  - router 분기 설정

  - cert.pem과 key.pem 파일을 통해 https 설정하기

 

express 코드 구현

- index.js 파일 코드

const express = require('express');
const cors = require('cors');
const session = require('express-session');
const fs = require('fs');
const https = require('https');
const usersRouter = require('./routes/user');

const app = express();

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

// express-session 라이브러리를 이용해 쿠키 설정
app.use(
  session({
    secret: '@codestates',
    resave: false,
    saveUninitialized: true,
    cookie: {
      domain: 'localhost',
      path: '/',
      maxAge: 24 * 6 * 60 * 10000,
      sameSite: 'none',
      httpOnly: true,
      secure: true,
    },
  })
);

app.use(express.json());

// 클라이언트가 어떤 origin에 따른 CORS 설정 (메서드 : GET, POST, OPTIONS)
app.use(cors({
  origin: 'https://localhost:3000',
  methods: ['GET', 'POST', 'OPTIONS'],
  credentials: true,
}));

// 라우터 설정
app.use('/users', usersRouter);

let server;
// 인증서 파일들이 존재하는 경우에만 https 프로토콜을 사용하는 서버를 실행
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;

 

- routes/user.js 코드 구현

var express = require('express');
var router = express.Router();

const { usersController } = require('../controller');

// * POST /users/login
router.post('/login', usersController.login.post);

// * POST /users/logout
router.post('/logout', usersController.logout.post);

// * get /users/userinfo
router.get('/userinfo', usersController.userinfo.get);

module.exports = router;

 


서버의 login 컨트롤러 제작

1. 클라이언트 요청으로 받은 userId, password와 일치하는 유저를 DB에서 검색하기

2. 해당 유저 정보가 존재하지 않으면, 실패 응답 전달하기

3. 해당 유저 정보가 존재한다면, 세션에 userId를 저장하고 성공 응답 보내기

  - post 요청에 대한 응답이므로, response body의 데이터는 null이 기본 값 { data: null }

  - 세션에 정보를 저장 : req.session.save(callback) 없이 객체 Dot Nation 문법만으로 사용 가능

 

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

module.exports = {
  post: async (req, res) => {
  
    // user 정보를 DB에서 조회
    const userInfo = await Users.findOne({
      where: { userId: req.body.userId, password: req.body.password },
    });

    // userInfo 결과 존재 여부에 따른 응답
    if (!userInfo) {
      res.status(400).send({ data: null, message: 'not authorized' });
    } else {
      req.session.save(function () {  // req.session.save(callback)은 사용하지 않아도 됨
        req.session.userId = userInfo.userId;
        res.json({ data: userInfo, message: 'ok' });
        // post 요청에 대한 응답이기에 {data:null}이 되므로, {data: userInfo} 무의미하여 생략 가능
      });
    }
  }
}

 

서버의 logout 컨트롤러 제작

1. 클라이언트의 요청의 Session 객체에 userId가 포함되어 있는지 확인하기 (로그인 여부 확인)

2. req.session.userId가 존재하지 않는다면, 실패 응답 보내기

3. req.session.userId가 존재한다면, Session을 삭제하고 성공 응답 보내기

 

module.exports = {
  post: (req, res) => {

    if (!req.session.userId) {
      res.status(400).send({ data: null, message: 'not authorized' });
    } else {
      req.session.destroy();      // 세션 삭제
      res.json({ data: null, message: 'ok' });
    }
  },
};

 

서버의 userInfo 컨트롤러 제작

1. 클라이언트의 get 요청에 대한 응답을 구현하기 위함 (post 요청의 응답으로는 userInfo 전달 불가능)

2. 클라이언트의 요청의 Session 객체에 userId가 포함되어 있는지 확인하기 (로그인 여부 확인)

3. req.session.userId가 존재하지 않는다면, 실패 응답 보내기

4. req.session.userId가 존재한다면, 그에 따라 해당 사용자 정보를 조회하여 response data로 전달하기

 

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

module.exports = {
  get: async (req, res) => {

    if (!req.session.userId) {
      res.status(400).send({ data: null, message: 'not authorized' });
    } else {
      const result = await Users.findOne({
        where: { userId: req.session.userId },
      }).catch((err) => res.json(err));

      res.status(200).json({ data: result, message: 'ok' });
    }
  },
};

 


클라이언트의 app.js 파일 제작

1. 상태(state)로 관리할 값 : 로그인 여부(Boolean), 유저의 데이터(object)

2. 메소드(setState) : 로그인 메소드, 로그아웃 메소드, 유저 데이터를 수정할 메소드

3. 로그인이 되어 있을 경우, Mypage 컴포넌트를 브라우저에 렌더링하기

4. 로그인이 안 되어 있을 경우, Login 컴포넌트를 브라우저에 렌더링하기

 

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() {    // 로그인 성공 시 로그인 상태 true로 변경
    this.setState({
      isLogin: true,
    });
  }

  setUserInfo(object) {    // 로그인 성공 시 user data 수정
    this.setState({ userData: object });
  }

  logoutHandler() {	   // 로그아웃 성공 시 로그인 상태 false로 변경
    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;

 


클라이언트의 Login 컴포넌트 제작

1. 유저가 입력할 username과 password를 상태로 관리하기

2. 유저의 키보드 입력에 따라 username과 password 를 변경할 이벤트 핸들러 제작

 

3. 서버에 로그인 요청을 위한 메소드 제작하기 (4~7)

4. username과 password를 요청 body에 담고, 서버에 로그인을 요청하기 (post 요청)

  - fetch를 통해 AJAX 요청이 가능하지만 여기서는 axios 사용

5. 로그인 상태를 true로 바꾸기 : 로그인 상태를 바꾸기 위한 loginHandler 사용하기

6. 로그인한 유저의 정보를 받기 위해 서버에 요청하기 (axios를 통한 get 요청)

  - 로그인 이후 처리되어야 하므로 .then 문법을 통해 순서대로 처리하기 

7. 서버로부터 받은 유저 정보를 바탕으로 유저 상태 변경 : setUserInfo 메소드 사용하기

 

import React, { Component } from 'react';
import axios from 'axios';
class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: '',  // 이벤트로 전달될 username
      password: '',  // 이벤트로 전달될 password
    };
    this.inputHandler = this.inputHandler.bind(this);
    this.loginRequestHandler = this.loginRequestHandler.bind(this);
  }

  inputHandler(e) {  // username과 password 입력을 위한 이벤트 핸들러
    this.setState({ [e.target.name]: e.target.value });
  }

  loginRequestHandler() {

    axios
      .post(   // 로그인을 위한 포스트 요청
        'https://localhost:4000/users/login',
        {
          // 서버의 login 컨트롤러의 객체 key의 명칭으로 인해 여기서도 'userId' 사용
          // 여기서 key 명칭을 userId가 아닌 username으로 하면 에러 발생
          userId: this.state.username,   
          password: this.state.password,
        },
        { 'Content-Type': 'application/json', withCredentials: true }
      )
      .then((res) => {
        this.props.loginHandler(true);
        // 유저 정보를 받기 위한 get 요청을 순서대로 진행
        return axios.get('https://localhost:4000/users/userinfo', {
          withCredentials: true,
        });
      })
      .then((res) => {
        let { userId, email } = res.data.data;
        this.props.setUserInfo({    // user data 상태 변경
          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;

 

클라이언트의 Mypage 컴포넌트 제작

1. 서버에 로그아웃 요청을 위한 메소드 제작하기

2. axios를 통해 post 요청하기

3. 로그인 상태를 바꾸기 위한 logoutHandler 사용하기

import React from 'react';
import axios from 'axios';

function Mypage(props) {
  const handleLogout = () => {  // 로그아웃을 위한 메소드 제작
    axios
      .post('https://localhost:4000/users/logout', null, {
        'Content-Type': 'application/json',
        withCredentials: true,
      })
      .then(() => props.logoutHandler())  // 로그인 상태 변경
      .catch((e) => alert(e));
  };

  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;

 


에러 핸들링

 

1. 쿠키가 전달되지 않아서 400, Bad Request 오류 발생

  - 해결방안 : axios 의 옵션으로 { withCredentials:true } 를 작성해야 쿠키 전달 가능

 

// Login 컴포넌트 중

axios.post('https://localhost:4000/users/login', {
  userId : this.state.username,      
  password: this.state.password
  }, {'Content-Type': 'application/json', withCredentials:true}
)
.then((res)=> {this.props.loginHandler();
  return axios.get('https://localhost:4000/users/userinfo', {
    withCredentials: true
  })
})
.then(res => this.props.setUserInfo(res.data.data))
.catch((err)=> alert(err));

 

// Mypage 컴포넌트 중

axios.post('https://localhost:4000/users/logout', null, {
  'Content-Type' : 'application/json', withCredentials: true
})
.then(res=>props.logoutHandler())
.then((err) => alert(err))

 

2. CORS origin의 URL 이 잘못 설정되어 오류 발생

- 원인 : React는 npm start 명령어에 따라서 클라이언트 쪽에서 https 로 작동되지만, CORS의 url은 http로 입력됨

- 해결방안 : CORS 설정시 http를 https로 변경

- 오류 내용 :  Access to XMLHttpRequest at 'https://localhost:4000/users/login' from origin 'https://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'http://localhost:3000' that is not equal to the supplied origin.

 

// server/index.js 파일 내용 중
// origin 주소를 'http://localhost:3000'로 기재하면 오류 발생

app.use(cors({
  origin: 'https://localhost:3000',
  methods: ['GET', 'POST', 'OPTIONS'],
  credentials: true,
}));

 

// client/package.json 파일 중

"scripts": {
    "start": 
    "HTTPS=true SSL_CRT_FILE='../server-session/cert.pem' 
    SSL_KEY_FILE='../server-session/key.pem' 
    react-scripts start"
}
반응형

댓글