React + Express로 JWT 방식 회원기능 구현하기 (2)

mhlog·2023년 4월 24일
0

React

목록 보기
5/10
post-thumbnail

지난 글에서 JWT 개념에 대해서 이해하였으니 이번 글에서는 React / TS 환경에서 JWT를 어떻게 구현하였는지를 포스팅하려 한다.

JWT Token 발행

사용자가 로그인을 하여 서버로 Client단에서 입력한 아이디와 비밀번호를 실어서 POST 요청을 하면 DB에 저장된 회원정보와 Client단에서 보내온 request.body의 값이 같다면 Token을 하나 만들어서 고객 브라우저의 쿠키에 저장한다. 다음은 위 기능을 구현하는 코드이다.

주의해야할 점은 Client단에서는 POST 요청을 할 때 withCredentials Option을 키고, 서버에서 쿠키를 주고 받을 수 있게 미들웨어를 추가해야한다. 이는 구글링해보면 빠르게 알 수 있으므로 생략하도록 하겠다. 또한, JWT는 decoding이 쉽기 때문에 secretKey를 길고 때려맞추기 어렵게 작성하여 .env파일로 따로 보관해야한다.

// routers/auth.js
const express = require('express');
const Router = express.Router();
const { login } = require('../controller/auth');

Router.post('/login', login);

// controller/auth.js
const login = async (req, res) => {
  try {
    // DB에 저장된 이메일이 없는 경우 status 402를 보냄.
    if (!await isemailExist(req.body.email)) throw 402;
    
    // DB에 해당 이메일이 있다면 비밀번호 검사를 수행함.
    const userInfo = await foundUserInfo(req.body.email);
    const isCorrectPW = await verifyPassword(req.body.password, userInfo.salt, userInfo.password);
    // 유저의 비밀번호가 맞지 않는 경우
    if (!isCorrectPW) throw 403;
    try {
      // 비밀번호까지 맞다면 access Token 발급
      const accessToken = jwt.sign({
        email: userInfo.email,
        name: userInfo.name
      }, process.env.ACCESS_TOKEN_SECRET, {
        expiresIn: '60m',
        issuer: 'Lee'
      })
      // token을 쿠키에 담아서 전송
      res.cookie('accessToken', accessToken, {
        secure: false,
        // JS에서 쿠키 접근이 불가능하게 하기 위함.
        httpOnly: true
      })
      res.status(200).json({
        name: userInfo.name,
        message: '로그인 성공하였습니다.'
      });
    } catch (error) {
      res.status(500).json(error);
    }
  } catch (errorcode) {
    if (errorcode === 402) {
      // 유저가 입력한 이메일이 DB에 없는 경우
      res.status(errorcode).json({ message: '해당 Email이 존재하지 않습니다.' });
    } else if (errorcode === 403) {
      // 유저가 입력한 비밀번호가 일치하지 않는 경우
      res.status(errorcode).json({ message: '비밀번호가 맞지 않습니다' })
    } else {
      res.status(500).json(error)
    }

  }
}

올바른 ID와 비밀번호로 POST 요청을 하니 다음과 같이 accessToken이 발행된 것을 확인할 수 있다.

JWT Token 유효성 검사

JWT Token의 유효성을 검사하는 부분에서 고민이 많았다. 처음 한 생각은 페이지 들어갈 때 한번만 로그인 여부를 검사하는 API에 GET 요청을 날려서 로그인 되었는지 여부와 user의 이름을 state로 관리하는 방식으로 생각하였는데, 이 방식으로 구현을 하게 되면 페이지에 머물다가 JWT Token이 만료가 되면 재로그인을 요구하는 alert창을 띄울 수가 없다. 생각해낸 해결 방법으로는 setInterval 함수를 사용해서 10분 (혹은 더 짧게)마다 로그인이 유효한지 서버에 요청을 보내는 방법이다. customHook으로 useIslogin 함수를 만들어서 구현하였다.

우선 로그인이 성공적으로 진행되었는지를 GET요청으로 확인할 수 있는 서버의 API는 다음과 같이 구현하였다.


// routers/auth.js
Router.get('/islogin', loginSuccess);

// controller/auth.js
const loginSuccess = async (req, res) => {
  try {
    const token = req.cookies.accessToken;
    // token이 존재하지 않음, 즉 유저가 로그인하지 않음
    if(!token) throw 400;
    const data = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);

    const userData = await foundUserInfo(data.email);
    const { password, salt, ...others } = userData;
    res.status(200).json({
      name: others.name
    });
  } catch (error) {
    if(error === 400) {
      res.status(400).json(error);
    } else {
      res.status(500).json(error);
    }
  }
}

다음은 Client단에서 CustomHook을 이용하여 10분을 주기로 서버에 GET 요청을 보내는 코드이다.

import { useEffect, useState } from "react";
import axios from "axios";

export default function useIsLogin() {
  const [islogin, setIsLogin] = useState(false);
  const [userName, setUserName] = useState('');
  const [status, setStatus] = useState(0);

  useEffect(() => {
    checkLoginStatus();
    // Check login every 10 minutes.
    const intervlId = setInterval(checkLoginStatus, 60 * 1000 * 10);
    return () => clearInterval(intervlId);
  }, [])


  const checkLoginStatus = () => {
    try {
      axios({
        url: "http://localhost:8080/auth/islogin",
        method: "GET",
        withCredentials: true
      })
        .then((response) => {
          setIsLogin(true);
          setStatus(200);
          setUserName(response.data.name);
        })
        .catch((error) => {
          if (error.response.status === 400) {
            // User does not login
            setStatus(400);
            setIsLogin(false);
            setUserName('');
          } else {
            // token expired
            setIsLogin(false);
            setUserName('');
            setStatus(500);
          }
        })
    } catch (error) {
      console.log(error)
    }
  }

  console.log(islogin, userName, status);
  return {
    islogin,
    userName,
    status
  };
}

결론

간단한 회원기능이지만 DB에 저장부터 해서 Token 발행 등 각잡고 구현하려고 하니 고려해야할 점이 많았다. 이번에는 accessToken의 시간을 짧게 두는 방식으로 구현하였지만, 다음 사이드 프로젝트에서는 refreshToken을 이용하여 구현해볼 예정이다!

0개의 댓글