포스트

2024.06.03 프로그래머스-북끼오 (bookkio) JWT 인증 과정

Bookkio 프로젝트 9일차

JWT 를 통해 구분을 해야하는 기능

  • 로그인
  • 좋아요 추가
  • 좋아요 취소
  • 장바구니

auth 유틸리티 함수 작성

(강의를 따라치지 않고, 개별적으로 생각해서 진행하였습니다)

이번 강의 과정에는 JWT 인증을 통해 기능을 사용할 수 있게 하는 부분에 대해 진행 되었는데,

JWT인증이 필요한 기능이 몇몇 있었다.

그러면 Controller마다 각 로직을 작성하는 것은 너무 불편한 일일 것 같아서

utils 폴더에 auth.js라는 파일에 사용자 검증을 위한 미들웨어 함수를 작성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const { verify } = require("jsonwebtoken");
var dotenv = require("dotenv");
dotenv.config();

const KEY = process.env.PRIVATE_KEY;

function validateJSONToken(token) {
  return verify(token, KEY);
}

/**
 *
 * @param {import("express").Request} req
 * @param {import("express").Response} res
 * @param {import("express").NextFunction} next
 */
function checkAuth(req, res, next) {
  if (req.method === "OPTIONS") {
    return next();
  }

  if (!req.headers.authorization) {
    console.log("NOT AUTH");
    return res.status(400).json({ message: "Not Auth" });
  }
  console.log(req.headers.authorization);
  const authFragment = req.headers.authorization.split(" ");
  console.log(authFragment);
  const authToken = authFragment[0];
  console.log(authToken);

  try {
    const validateToken = validateJSONToken(authToken);
    console.log(validateToken);
    req.token = validateToken;
  } catch (err) {
    return res.status(400).json({ message: "유효하지 않은 사용자 정보" });
  }
  next();
}

exports.checkAuth = checkAuth;

위의 checkAuth 함수를 이제 미들웨어로 사용하면 된다.

좋아요 API AUTH 미들웨어 추가

  • likes.route.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      	const express = require("express");
      const { checkAuth } = require("../utils/auth.js");
      const router = express.Router();
        
      const { addLike, deleteLike } = require("../controller/likes.controller.js");
        
      // 인증 미들웨어
      router.use(checkAuth);
      router.post("/:bookId", addLike);
      router.delete("/:bookId", deleteLike);
        
      module.exports = router;
        
    

위에서 작성한 checkAuth 함수를 likes.route.js 에 추가하여, likes 쪽 URI에 요청이 오면 무조건 사용자 인증 과정을 거치도록 해두었다.

때문에 좋아요를 누를 때, 사용자 인증이 올바르게 됐다면, request의 token 객체에 사용자에 대한 정보 값이 작성 되어있을 것이므로 likes.controller.js 에도 token을 사용하여 작업을 진행하였다.

(사용자 인증이 되지 않는 token이면, 애초에 likes 라우터에는 접근이 안되는 원리!)

  • likes.controller.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    
      const { verify } = require("jsonwebtoken");
      const dbConnection = require("../model/mysql.js");
      const { StatusCodes } = require("http-status-codes");
      const dotenv = require("dotenv");
      dotenv.config();
        
      /**
       * 좋아요 추가 API
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       */
      const addLike = (req, res) => {
        const { bookId } = req.params;
        const { token } = req;
        // let userJwt = req.headers.authorization;
        // console.log("receiver JWT : ", userJwt);
        
        // let verifiedJwt = verify(userJwt, process.env.PRIVATE_KEY);
        // console.log(verifiedJwt);
        const queryArg = [+token.id, +bookId];
        
        let sqlQuery = `
        SELECT * FROM likes
        WHERE user_id = ? AND book_id = ?;
        `;
        
        dbConnection.query(sqlQuery, queryArg, (err, result) => {
          if (err) return res.status(StatusCodes.BAD_REQUEST).end();
        
          if (result.length === 0) {
            sqlQuery = `
              INSERT INTO likes (user_id, book_id)
              VALUES (?, ?);
              `;
            dbConnection.query(sqlQuery, queryArg, (err, result) => {
              if (err) {
                return res
                  .status(StatusCodes.BAD_REQUEST)
                  .json({ message: "잘못된 요청정보 입니다." });
              }
        
              return res.status(StatusCodes.OK).json(result.affectedRows);
            });
          } else {
            return res
              .status(StatusCodes.BAD_REQUEST)
              .json({ message: "이미 좋아요를 눌렀습니다." });
          }
        });
      };
        
      /**
       * 좋아용 제거 API
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @returns
       */
      const deleteLike = (req, res) => {
        const { token } = req;
        const { bookId } = req.params;
        let sqlQuery = `
          DELETE FROM likes
          WHERE user_id=? AND book_id=?;
        `;
        dbConnection.query(sqlQuery, [+token.id, +bookId], (err, result) => {
          if (err) {
            return res
              .status(StatusCodes.BAD_REQUEST)
              .json({ message: "올바르지 않은 요청 입니다." });
          }
        
          return res.status(StatusCodes.OK).json(result.affectedRows);
        });
      };
        
      module.exports = {
        addLike,
        deleteLike,
      };
        
    
    • likes.controller.js에서는 인증 미들웨어로 설정된 req.token 의 값을 이용하여 사용자의 id를 받아 사용하였다.
    • 인증 되지 않았다면, 애초에 likes의 기능을 사용할 수 없게 제한 하였다.

장바구니 기능 API에 AUTH 적용 하기

  • cart.route.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      const express = require("express");
      const {
        addItemToCart,
        getAllCartItems,
        decreaseCartItem,
        removeCartItem,
      } = require("../controller/cart.controller");
      const { checkAuth } = require("../utils/auth");
      const router = express.Router();
        
      router.use(checkAuth);
      router
        .route("/")
        .post(addItemToCart)
        .get(getAllCartItems)
        .delete(removeCartItem);
      router.post("/decrease", decreaseCartItem);
        
      module.exports = router;
        
    
    • cart.route.js에도 likes와 같이 checkAuth 를 미들웨어로 추가하여, 인증과정을 우선적으로 수행하게 설정하였다.
  • cart.controller.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    
      const dbConnection = require("../model/mysql.js");
      const { StatusCodes } = require("http-status-codes");
        
      function getIdFromToken(req) {
        const { token } = req;
        const { id } = token;
        return +id;
      }
        
      /**
       * 장바구니에 아이템 담기 로직
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @param {import("express").NextFunction} next
       */
      const addItemToCart = (req, res, next) => {
        const { bookId } = req.body;
        const userId = getIdFromToken(req);
        
        let sqlQuery = `
          SELECT * FROM cartitems
          WHERE user_id=? AND book_id=?;
        `;
        let queryArg = [+userId, +bookId];
        
        dbConnection.query(sqlQuery, queryArg, (err, results) => {
          if (err) {
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          if (results.length > 0) {
            // 장바구니에 이미 있는 경우 개수 증가
            const existItem = { ...results[0], qty: results[0].qty + 1 };
            sqlQuery = `
              UPDATE cartitems
              SET qty=?
              WHERE id = ? 
            `;
            queryArg = [existItem.qty, existItem.id];
        
            dbConnection.query(sqlQuery, queryArg, (err, results) => {
              if (err) {
                return res.status(StatusCodes.INTERNAL_SERVER_ERROR).end();
              }
        
              return res.status(StatusCodes.OK).json(existItem);
            });
          } else {
            // 장바구니에 없는 경우 => 새로 추가하는 상품
            sqlQuery = `
              INSERT INTO cartitems (book_id, user_id, qty)
              VALUES (?,?,?);
            `;
            queryArg = [+bookId, +userId, 1];
        
            dbConnection.query(sqlQuery, queryArg, (err, results) => {
              if (err) {
                return res.status(StatusCodes.INTERNAL_SERVER_ERROR).end();
              }
        
              return res.status(StatusCodes.OK).json({ result: results });
            });
          }
        });
      };
        
      /**
       * 장바구니 전체조회 로직 => 사용자 한 명에 대하여
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @param {import("express").NextFunction} next
       */
      const getAllCartItems = (req, res, next) => {
        const userId = getIdFromToken(req);
        let sqlQuery = `
        SELECT cartitems.id AS cart_id, 
        books.price AS book_price, 
        cartitems.qty AS cart_qty, 
        cartItems.user_id AS user_id, 
        cartItems.book_id AS book_id   
        FROM cartitems LEFT 
        JOIN books ON books.id = cartitems.book_id
        WHERE cartitems.user_id = ?;
        `;
        let queryArg = [+userId];
        
        dbConnection.query(sqlQuery, queryArg, (err, results) => {
          if (err) {
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          if (results.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).end();
          }
          return res.status(StatusCodes.OK).json(results);
        });
      };
        
      /**
       * 카트 아이템 수량 감소
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       */
      const decreaseCartItem = (req, res) => {
        const { bookId } = req.body;
        const { token } = req;
        const { id: userId } = token;
        
        let sqlQuery = `
        SELECT * FROM cartitems
        WHERE user_id=? AND book_id=?;  
        `;
        
        let queryArg = [+userId, +bookId];
        
        dbConnection.query(sqlQuery, queryArg, (err, result) => {
          if (err) {
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          const existItem = result[0];
          if (existItem) {
            //카트에 아이템 존재
            if (existItem.qty > 1) {
              // 수량 감소
              sqlQuery = `
              UPDATE cartitems
              SET qty = ?
              WHERE id = ?
              `;
              queryArg = [existItem.qty - 1, existItem.id];
        
              dbConnection.query(sqlQuery, queryArg, (err, result) => {
                if (err) {
                  return res.status(StatusCodes.INTERNAL_SERVER_ERROR).end();
                }
        
                return res.status(StatusCodes.OK).json(result.affectedRows);
              });
            } else {
              sqlQuery = `
              DELETE FROM cartitems
              WHERE id=?
              `;
              queryArg = [existItem.id];
              dbConnection.query(sqlQuery, queryArg, (err, result) => {
                if (err) {
                  return res.status(StatusCodes.INTERNAL_SERVER_ERROR).end();
                }
                return res.status(StatusCodes.OK).json(result);
              });
            }
          }
        });
      };
        
      /**
       * 장바구니 아이템 삭제 로직
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       */
      const removeCartItem = (req, res) => {
        const { bookId } = req.body;
        const userId = getIdFromToken(req);
        
        let sqlQuery = `
        DELETE FROM cartitems
        WHERE user_id=? AND book_id=?;
        `;
        
        let queryArg = [+userId, +bookId];
        
        dbConnection.query(sqlQuery, queryArg, (err, result) => {
          if (err) {
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          return res.status(StatusCodes.OK).json(result);
        });
      };
        
      module.exports = {
        addItemToCart,
        getAllCartItems,
        decreaseCartItem,
        removeCartItem,
      };
        
    
    • 인증을 통해 설정된 token에서 id (userId)를 추출하여 장바구니 기능을 사용할 수 있도록 설정하였다.
    • id를 추출하는 부분이 공통 로직이므로 function으로 만들어 사용하였다.

Token 의 세부 에러 핸들링

앞서 작성한 auth.js 미들웨어에서, token의 expired 가 만료되거나 유효하지 않은 토큰일 때 를 구별하여 에러를 return 하는 과정을 추가하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const {
  verify,
  TokenExpiredError,
  JsonWebTokenError,
} = require("jsonwebtoken");
const { StatusCodes } = require("http-status-codes");

var dotenv = require("dotenv");
dotenv.config();

const KEY = process.env.PRIVATE_KEY;

function validateJSONToken(token) {
  return verify(token, KEY);
}

/**
 *
 * @param {import("express").Request} req
 * @param {import("express").Response} res
 * @param {import("express").NextFunction} next
 */
function checkAuth(req, res, next) {
  if (req.method === "OPTIONS") {
    return next();
  }

  if (!req.headers.authorization) {
    console.log("NOT AUTH");
    return res.status(400).json({ message: "Not Auth" });
  }
  console.log(req.headers.authorization);
  const authFragment = req.headers.authorization.split(" ");
  console.log(authFragment);
  const authToken = authFragment[0];
  console.log(authToken);

  try {
    const validateToken = validateJSONToken(authToken);
    console.log(validateToken);
    req.token = validateToken;
  } catch (err) {
    if (err instanceof TokenExpiredError) {
      return res
        .status(StatusCodes.UNAUTHORIZED)
        .json({ message: "로그인 세션이 만료 되었으니 다시 로그인해 주세요" })
        .end();
    } else if (err instanceof JsonWebTokenError) {
      return res
        .status(StatusCodes.BAD_REQUEST)
        .json({ message: "올바르지 않은 토큰 정보 입니다." })
        .end();
    }
  }
  next();
}

exports.checkAuth = checkAuth;

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.