[TIL] Access Token, Refresh Token, Redis 이용해서 인증 구현하기

sooyoung choi·2023년 12월 14일
0

Javascript, Node.js

목록 보기
36/37
post-thumbnail

벌써 4번째 프로젝트!! 이번에 맡은 부분은 회원 인증 파트였는데 처음 써보는 Refresh Token, 그 Refresh Token을 Redis에 저장하는 과정을 기록해본다.

⚙ Redis 설치

1) msi 파일 다운받아 설치를 진행한다.

  • 설치 진행하면 redis 서버는 자동으로 돌아간다.

2) 관련 package 설치

  • redis, @redis/client, jsonwebtoken, cookie-parser
yarn add redis @redis/client jsonwebtoken cookie-parser

3) redis 연결해주기


// ./auth-utils/redis.util.js

import redis from 'redis';

const redisClient = redis.createClient(process.env.REDIS_PORT);

redisClient.on('connect', () => console.log('Connected to Redis!'));
redisClient.on('error', (err) => console.log('Redis Client Error', err));
redisClient.connect();

export default redisClient;


🔑 Access Token, Refresh Token 만들기

  • 로그인 시 쿠키에 Access Token과 Refresh Token을 저장해줘야 한다.
  • 로그아웃 시 쿠키와 서버(redis)에 저장되어있는 토큰 값들을 지워줘야 한다.

1. 토큰 발급하기

  • jwt.sign() 을 사용한다.
const token = jwt.sign({ user_id }, process.env.ACC_TOKEN_KEY);

console.log(token);	
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo0LCJpYXQiOjE3MDI1NjAyNDIsImV4cCI6MTcwMjU2MDU0Mn0.Sr-6iTAeBuSzdHbPlj6MDzab7aTlB53oMZ8CwdksIaI 
  • refresh token은 redis에 저장해야하니 redis client를 사용하여 저장해주었다.

2. 로그인과 로그아웃에 적용하기

// 전체적인 login, logout 로직

 login = async (req, res, next) => {
        try {
            const { email, password } = req.body;

            const user = await this.authService.login(email, password);
			
          	// user{
          	// 	user{
            //		id: 1, 
            const user_id = user.user.id;
          
            if (!user) {
                throw error;
            }

          
          	// Access Token 발급
            const accessToken = jwt.sign(
                { user_id },
                process.env.ACC_TOKEN_KEY,	// 토큰 키
                {
                    expiresIn: process.env.ACCESS_EXP_IN,	// 만료 유효 기간
                },
            );
          
           // Refresh Token 발급
            const refreshToken = jwt.sign(
                { user_id },
                process.env.REF_TOKEN_KEY,
                {
                    expiresIn: process.env.REFRESH_EXP_IN,
                },
            );

            // Access Token 쿠키 저장
            res.cookie('accessToken', accessToken);
          
            // Refresh Token 쿠키 저장
            res.cookie('refreshToken', refreshToken);
          
           // Refreshtoken redis 저장 (key, value)
            redisClient.set(refreshToken, user_id);

            return res.status(200).json({
                message: '로그인 성공',
                data: { accessToken, refreshToken },
            });
          
        } catch (error) {
            console.log(error);
            next(error);
        }
    };

    logout = async (req, res, next) => {
            try {
                // Access Token 및 Refresh Token 변수 선언
                const accessToken = req.cookies.accessToken;
                const refreshToken = req.cookies.refreshToken;

              	// 쿠키에 담은 토큰들 삭제
                res.clearCookie('accessToken');
                res.clearCookie('refreshToken');

                // Redis에서 토큰들 삭제
                redisClient.del(refreshToken);
                redisClient.del(accessToken);

                return res.status(200).json({
                    message: '로그아웃 성공',
                });
            } catch (err) {
                next(err);
            }
        };


⛳ 인증이 필요한 router

  • 사용자 정보 조회 시 자신의 정보만을 볼 수 있으려면 인증 절차가 필요하다.
// ./src/routes/user.router.js

import express from 'express';
import { UsersController } from '../modules/users/users.controller.js';
import authMiddleware from '../middlewares/auth.middleware.js';

const router = express.Router();
const usersController = new UsersController();

// 사용자 회원가입
router.post('/signup', usersController.signup);

// 사용자 정보
// authMiddleware 에서 token 인증을 해줄 것임!
router.get('/:id', authMiddleware, usersController.getUser);

export default router;


🧶 미들웨어 구현하기


// ./src/middlewares/auth.middleware.js

import jwt from 'jsonwebtoken';
import { prisma } from './../utils/prisma/index.js';
import redisClient from '../../auth-utils/redis.util.js';
export default async (req, res, next) => {
    try {
      
       // cookie에 저장되어있는 토큰 값들을 가져온다.
        const { accessToken, refreshToken } = req.cookies;
      
      // Access Token 검증하기
        const accessPayload = validToken(
            accessToken,
            process.env.ACC_TOKEN_KEY,
        );

        // Refresh Token 검증하기
        const refreshPayload = validToken(
            refreshToken,
            process.env.REF_TOKEN_KEY,
        );
      
      // redis에 저장한 refresh token 가져오기
        const redisRefreshToken = await redisClient.get(refreshToken);

      // case 1) access token이 없을때
        if (!accessToken) {
            return res.status(401).json({ message: '다시 로그인 해주세요.' });
        }

        // case 2) accesstoken이 만료되고, refreshtoken은 있을때
        if (!accessPayload) {
            if (
                refreshPayload &&
                Number(redisRefreshToken) === jwt.decode(refreshToken).user_id
            ) {
                const { user_id } = refreshPayload;
                const newAccessToken = jwt.sign(
                    { user_id },
                    process.env.ACC_TOKEN_KEY,
                );
                res.cookie('accessToken', newAccessToken);
            }

            return res
                .status(200)
                .json({ message: 'ACCESS TOKEN이 갱신 되었습니다.' });

        }

        // case 3) refreshtoken 없을때, accesstoken이 인증되었다면 새로운 refreshtoken 발급해주기
        if (!refreshToken) {
            if (accessPayload) {
                const { user_id } = accessPayload;
                const newRefreshToken = jwt.sign(
                    { user_id },
                    process.env.REF_TOKEN_KEY,
                );
                res.cookie('refreshToken', newRefreshToken);
            }
            return res
                .status(401)
                .json({ message: 'REFRESH TOKEN이 존재하지 않습니다.' });
        }

      // case 4) redis에 refresh token 없을 때
        if (!redisRefreshToken) {
            return res
                .status(401)
                .json({ message: 'REFRESH TOKEN이 서버에 존재하지 않습니다.' });
        }

        // case 5) accesstoken은 있고, refreshtoken 만료되었을때
        if (!refreshPayload) {
            if (
                accessPayload &&
                Number(redisRefreshToken) === jwt.decode(accessToken).user_id
            ) {
                const { user_id } = accessPayload;
                const newRefreshToken = jwt.sign(
                    { user_id },
                    process.env.REF_TOKEN_KEY,
                );
                res.cookie('refreshToken', newRefreshToken);
            }
            return res
                .status(200)
                .json({ message: 'REFRESH TOKEN이 갱신 되었습니다.' });

        }

        const { user_id } = accessPayload;
        const user = await prisma.users.findUnique({ where: { id: user_id } });

        res.locals.user = user;
        next();
    } catch (error) {
        next(error);
    }
};

// 토큰 검증 함수
function validToken(token, secretKey) {
    try {
        const payload = jwt.verify(token, secretKey);
        return payload;
    } catch (error) {
        console.log(error);
    }
}

0개의 댓글