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 라이센스를 따릅니다.