Token으로 로그인 상태 저장하기

Kim-DaHam·2023년 5월 4일
0

Server

목록 보기
2/10
post-thumbnail

🔥 학습목표

  • Token을 사용하여 사용자의 로그인 상태를 저장한다.



🟩 환경변수 설정

.env 파일을 생성하여 액세스 토큰과 리프레시 토큰에 사용할 Salt를 정의한다.

🟩 Server

🟣 Express 서버 구축하기

⬜ 필요한 모듈

const express = require('express');
const cors = require('cors');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const https = require('https');
const controllers = require('./controllers');
const app = express();
  • 인증 토큰을 쿠키에 저장하기 위해 cookie-parser 를 사용한다.

⬜ 요청 메서드 라우터

app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);



🟣 Controller

마찬가지로 이렇게 구성 되어있다.


⬜ 로그인 POST 요청

필요한 상태 관리

const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
  • 클라이언트에서 POST 요청 시, loginInfo 객체에 유저 아이디와 비밀번호가 request body로 담겨 온다.

  • 로그인 유지 옵션을 체크했으면 true, 아니면 false 값이 넘어온다.


사용자 계정 데이터

const userInfo = {
    ...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
  };

저장 된 회원 데이터 중 아이디와 비밀번호가 일치한 회원 정보를 불러온다.


주요 기능

여기서부터 쿠키/세션 실습과 비교하면 좋다.

 if (!userInfo.id) {
    res.status(401).send('Not Authorized');
  }

유저 정보가 존재하지 않는다면 에러 메세지를 출력한다.


  const { accessToken, refreshToken } = generateToken(userInfo, checkedKeepLogin);
  • 토큰 생성 함수를 호출하여 사용자 정보를 담은 두 개의 토큰을 생성한다.

generateToken 함수에 대해선 후에 설명한다.


  if (refreshToken) {
    res.cookie('refresh_jwt', refreshToken, {
      domain: 'localhost',
      path: '/',
      sameSite: 'strict',
      httpOnly: true,
      secure: true,
      expires: new Date(Date.now() + 24 * 3600 * 1000 * 7), // 7일 후 소멸되는 Persistent Cookie
    });
  }
  • 리프레시 토큰이 존재한다면 refresh_jwt 라는 이름의 쿠키를 전송한다. 쿠키에는 리프레시 토큰을 저장하며 해당 쿠키는 7일 후 소멸되는 expires 옵션을 갖고있다. (Presistent Cookie)

  res.cookie('access_jwt', accessToken, {
    domain: 'localhost',
    path: '/',
    sameSite: 'strict',
    httpOnly: true,
    secure: true,
    // Expires 옵션이 없는 Session Cookie
  });
  return res.redirect('/userinfo');
};
  • 리프레시 토큰이 있든 없든 일단 액세스 토큰은 쿠키로 무조건 보낸다. 이때 액세스 쿠키는 expires 옵션이 없는 세션 쿠키에 해당한다.

  • 쿠키를 전송한 후에는 로그인한 사용자 정보를 불러오기 위해 /userinfo 경로로 리다이렉트 한다.


⬜ 유저 정보 GET 요청

const accessToken = req.cookies['access_jwt'];
const refreshToken = req.cookies['refresh_jwt'];
const accessPayload = verifyToken('access', accessToken);
  • 유저 정보를 전달하려면 리소스를 요청한 사용자가 유효한 액세스 토큰을 갖고있는지 검증해야 한다.

  • req.cookies로 액세스 토큰과 리프레시 토큰을 가져온 뒤, 액세스 토큰에 대하여 verifyToken 함수로 유효성 검증을 실행한다.

해당 함수에 대해선 나중에 설명한다.


if (accessPayload) {
    const userInfo = { ...USER_DATA.filter((user) => user.id === accessPayload.id)[0] };
    if (!userInfo) {
      return res.status(401).send('Not Authorized');
    }
    delete userInfo.password;
    return res.json(userInfo);
  } else if (refreshToken) {
    const refreshPayload = verifyToken('refresh', refreshToken);
    ...
  • 유효한 액세스 토큰을 가지고 있는 사용자라면, Payload에 저장 된 id 와 일치한 정보를 가져온다.

  • 만약 일치하는 사용자 정보가 없다면 401 에러 메세지를 보낸다.

  • 사용자 인증을 끝냈으니 이제 정보를 전달해줘야 한다. 가지고 있는 사용자 정보 중 민감한 정보인 password를 제거하고 json 형식으로 전달한다.

  • 만약 리프레시 토큰을 가지고 있는 사용자라면 리프레시 토큰에 대해서도 검증(verifyToken) 해야한다.


    if (!refreshPayload) {
      return res.status(401).send('Not Authorized');
    }
  • 만약 유효한 리프레시 토큰이 아니라면 에러 메세지를 출력한다.

    const userInfo = USER_DATA.filter((user) => user.id === refreshPayload.id)[0];
    const { accessToken } = generateToken(userInfo);
  • 유효한 리프레시 토큰이라면 방금 전과 같이 사용자 정보를 뽑아내고 액세스 토큰을 재생성 해준다.

    res.cookie('access_jwt', accessToken, {
      domain: 'localhost',
      path: '/',
      sameSite: 'strict',
      httpOnly: true,
      secure: true,
      // Expires 옵션이 없는 Session Cookie
    });
  • 액세스 토큰을 access_jwt라는 이름의 쿠키로 전송한다. 처음 생성했을 때와 마찬가지로 Expires 옵션이 없는 세션 쿠키로 설정한다.

    return res.json({ ...userInfo, password: undefined });
  }
  • 마지막으로 모든 검증 단계가 끝나면 passwordundefined로 바꾼 뒤 사용자 정보를 json 형태로 전달한다.

return res.status(401).send('Not Authorized');
};
  • 액세스 토큰도, 리프레시 토큰도 유효하지 않다면 401 에러 메세지를 보낸다.

⬜ 로그아웃 POST 요청

쿠키 삭제

const refreshToken = req.cookies['refresh_jwt'];
  if (refreshToken) {
    res.clearCookie('refresh_jwt', {
      domain: 'localhost',
      path: '/',
      sameSite: 'strict',
      secure: true,
    });
  }
  res.clearCookie('access_jwt', {
    domain: 'localhost',
    path: '/',
    sameSite: 'strict',
    secure: true,
  });
  return res.status(205).send('Logged Out Successfully');
  • req.cookies로 쿠키에 저장된 리프레시 토큰을 가져온 뒤, 리프레시 토큰이 존재하면 refresh_jwt 쿠키를 삭제하고, 존재하지 않으면 액세스 토큰 쿠키인 access_jwt만 삭제한다.

🟧 토큰 생성 및 검증

require('dotenv').config();
const { sign, verify } = require('jsonwebtoken');
  • dotenv : 환경변수를 .env 파일에 저장하고 process.env 로 로드하는 의존성 모듈

  • jsonwebtoken : JWT 토큰을 자동으로 생성해주는 메서드가 담긴 모듈
    (npm install jsonwebtoken 로 설치한다)


generateToken

(user, checkedKeepLogin) => {
    const payload = {
      id: user.id,
      email: user.email,
    };
  
    let result = {
      accessToken: sign(payload, process.env.ACCESS_SECRET, {
        expiresIn: '1d', // 1일간 유효한 토큰을 발행
      }),
    };

    if (checkedKeepLogin) {
      result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
        expiresIn: '7d', // 일주일간 유효한 토큰을 발행
      });
    }
    return result;
  }
  • 토큰의 payload 부분에 저장할 사용자 정보를 정의한다.

  • sign : 토큰을 만들어 클라이언트에 발급해주는 메서드. 전송 데이터 payload와 .env 파일에 저장한 Salt 값을 인자로 전달한다.

  • 액세스 토큰의 유효기간은 하루, 리프레시 토큰의 유효기간은 일주일이다. (리프레시 토큰은 사용자가 로그인 유지하기 체크박스를 체크했을 때만 생성된다.


verifyToken

(type, token) => {
    let secretKey, decoded;
    switch (type) {
      case 'access':
        secretKey = process.env.ACCESS_SECRET;
        break;
      case 'refresh':
        secretKey = process.env.REFRESH_SECRET;
        break;
      default:
        return null;
    }

    try {
      decoded = verify(token, secretKey);
    } catch (err) {
      console.log(`JWT Error: ${err.message}`);
      return null;
    }
    return decoded;
  }
  • verify : 발급받은 토큰이 제대로 만들어진 토큰인지 확인해주는 메서드. 발급 받은 토큰과 비밀 키를 인자로 전달한다.



profile
다 하자

0개의 댓글