지금까지는 사용자(로그인이 된 회원)의 권리를 가진 사용자만이 할수있는 좋아요추가/취소, 장바구니담기/조회 등을 Request(요청)할때 단순히 유저id 또는 이메일 등의 방식으로 요청을 했습니다. 이번에는 이러한 방식에서 벗어나 JWT를 이용해 Header에 JWT를 담아서 서버에 보내고 요청을 할 수 있도록 설계해보겠습니다.
이전에 한번 jwt가 무엇인지 어떻게 발행하고 어떻게 검증하는지에 대해 다뤄봤습니다.
https://myeongsu0257.tistory.com/231
이번에는 좀 더 자세히 어떻게 cookie에 실어서 클라이언트로 token을 보내는지 알아보겠습니다. 먼저 발행입니다.
token을 발급하고 cookie에 담아서 보내는데 postman으로 확인해보겠습니다.
const express = require('express');
const app = express();
const dotenv = require('dotenv');
let jwt=require('jsonwebtoken');
dotenv.config();
app.listen(process.env.PORT);
// GET + "/jwt" : 토큰 발행
app.get('/jwt', function (req, res) {
let token = jwt.sign({foo:'bar'},process.env.PRIVATE_KEY);
console.log(token);
res.cookie("jwt",token,{
httpOnly:true
});
res.send('토큰 발행');
});
요청을 해보니 이상한점을 발견할 수 있습니다. Request Headers에 Cookie값이 있었습니다. 저는 분명 Cookie값을 보낸적이 없는데?
일단 한번 더 요청해봤습니다. 그랬더니 똑같이 Request에 Cookie가 또 담겨있었습니다. 하지만 자세히보니 Request의 Cookie값이 바로 전 요청에 있는 Reoseponse의 Set-Cookie값과 똑같음을 알게 되었습니다.
이 현상으로 인해 발생할 수 있는 문제점은 이렇게 쿠키에 계속 다른값을 담아주고 이전의 쿠키값과 혼동이되어 문제가발생할 수 있습니다. 그래서평소에 사이트를 이용하면서 쿠키삭제와같은 것들을 해주면 오류가 해결되는 현상이 있었습니다.
결과적으로 토큰을 쿠키에 발급할고 다음번에 토큰이 필요한 요청을 할때 header에 Authorization에 토큰의 값을 담아서 보내주면 됩니다.
const express = require("express");
const app = express();
const dotenv = require("dotenv");
let jwt = require("jsonwebtoken");
dotenv.config();
app.listen(process.env.PORT);
// GET + "/jwt" : 토큰 발행
app.get("/jwt", function (req, res) {
let token = jwt.sign({ foo: "bar" }, process.env.PRIVATE_KEY);
console.log(token);
res.cookie("jwt", token, {
httpOnly: true,
});
res.send("토큰 발행");
});
// GET + "/jwt/decoded" : 토큰을 검증
app.get("/jwt/decoded", function (req, res) {
let receivedJwt = req.headers["authorization"];
console.log(receivedJwt);
let decoded = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
res.json(decoded);
});
토큰을 발행하고 해당 토큰값을 postman의 콘솔의 response의 set-cookie의 jwt= 여기값을 header에 실어서 보내주면됩니다. 참고로 ;(세미콜론)은 빼야 됩니다.( 이걸로 시간을 좀 잡아먹었습니다...)
JWT 프로젝트에 적용
좋아요
프로젝트에서 좋아요 api에서 Request Body에 token을 보내서 접근이 가능한 사용자인지 아닌지 확인하는 부분을 설계해보겠습니다. 먼저 이전에 설계한 api는 body에 user_id값을 받아서 확인을 했었는데 아래 명세처럼 jwt를 받아서 확인해보겠습니다.
좋아요 추가
Method | POST |
URI | /likes/{bookdId} |
HTTP status code | 200 |
Request Body | // 로그인할 때 받은 token > req.header "Authorization" // payload 값을 읽는다 -> 사용자의 id를 읽어낼 수 있다. |
Response Body |
좋아요 취소
Method | DELETE |
URI | /likes/{bookdId} |
HTTP status code | 200 |
Request Body | // 로그인할 때 받은 token > req.header "Authorization" // payload 값을 읽는다 -> 사용자의 id를 읽어낼 수 있다. |
Response Body |
이전 코드입니다. 2개의함수 좋아요추가(addLike), 좋아요삭제(removeLike) 두개 다 모두 body에 user_id를 받아서 sql문을 실행했었습니다.
const conn = require("../mariadb");
const {StatusCodes}= require('http-status-codes');
const addLike=(req,res)=>{
const {id}=req.params;
const {user_id}=req.body;
const sql = `INSERT INTO likes VALUES(?,?)`;
let values=[user_id, Number(id)];
conn.query(sql,values,
(err,results,fileds)=>{
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
})
}
const removeLike = (req,res)=>{
const {id} = req.params;
const {user_id} = req.body;
const sql = `DELETE FROM likes WHERE user_id= ? AND liked_book_id=?`;
let value = [user_id,Number(id)]
conn.query(sql,value,
(err,results,fields)=>{
if(err){
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.OK).json(results);
})
}
module.exports={
addLike,
removeLike
}
jwt를 이용해서 바뀐코드를 보기전에 로그인을 할때 어떤값을 payload에 담아서 보내줄건지 정해야합니다. 저는 아래와 같이 유저의 email과 id값을 담아서 보내줍니다.
아래의 코드에서 receivedJwt에 Request의 Header에서 전송한 Authorization의 토큰값을 저장하고 해당 값을 이용해 복호화를 한후 decodedJwt에 담아주면 해당변수에는 Payload의 값이 담깁니다.
....
const jwt = require("jsonwebtoken");
const addLike = (req, res) => {
const { id } = req.params;
const receivedJwt = req.headers["authorization"];
const decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
const sql = `INSERT INTO likes VALUES(?,?)`;
let values = [decodedJwt.id, Number(id)];
conn.query(sql, values, (err, results, fileds) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
});
};
const removeLike = (req, res) => {
const { id } = req.params;
const receivedJwt = req.headers["authorization"];
const decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
const sql = `DELETE FROM likes WHERE user_id= ? AND liked_book_id=?`;
let values = [decodedJwt.id, Number(id)];
conn.query(sql, values, (err, results, fields) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.OK).json(results);
});
};
....
아래처럼 console에 decodedJwt를 찍어보았을때 email과 id가 잘 담겨져 있는 것을 확인할 수 있습니다. 그래서 해당값에서 id를 value에 담아서 사용해주면됩니다.
위의 코드에서 중복되는 부분을 함수로 만들어주고 나머지들도 정리한 최종본코드입니다.
const conn = require("../mariadb");
const { StatusCodes } = require("http-status-codes");
const jwt = require("jsonwebtoken");
const addLike = (req, res) => {
const bookId = req.params.id;
const decodedJwt = decodeJwt(req);
const sql = `INSERT INTO likes VALUES(?,?)`;
let values = [decodedJwt.id, Number(bookId)];
conn.query(sql, values, (err, results, fileds) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
});
};
const removeLike = (req, res) => {
const bookId = req.params.id;
const decodedJwt = decodeJwt(req);
const sql = `DELETE FROM likes WHERE user_id= ? AND liked_book_id=?`;
let values = [decodedJwt.id, Number(bookId)];
conn.query(sql, values, (err, results, fields) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.OK).json(results);
});
};
function decodeJwt(req) {
const receivedJwt = req.headers["authorization"];
return jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
}
module.exports = {
addLike,
removeLike,
};
JWT 토큰만료 예외처리
토큰을 Header에 넣어서 요청을 해주다가 아래처럼 JWT가 만료되었다고 나옵니다.(만료시간이 지났기때문) 이럴때는 예외처리를 해줘야 합니다.
아래는 JWT가 발생할 수 있는 대표적인 에러 2가지입니다. 이 에러에 대해 try-catch문을 이용해 예외처리를 해주겠습니다.
- TokenExpiredError : 유효기간이 지난 토큰 = 만료된 토큰
- JsonWebToken : 문제 있는 토큰
먼저 TokenExpiredError입니다. 복호화하는 함수에서 try-catch문을 이용해 정상적이면 복호화된 jwt값을 에러가 발생하면 err를 리턴해주었습니다.
function decodeJwt(req, res) {
try {
const receivedJwt = req.headers["authorization"];
return jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
} catch (err) {
console.log(err);
return err;
}
}
위의 함수를 호출하는 api에서는 if-else문과 instanceof를 이용해 토큰이 만료되었으면 message를 아니면 기존에 sql문을 실행하도록 해주었습니다.
const addToCart = (req, res) => {
const { book_id, quantity } = req.body;
const decodedJwt = decodeJwt(req, res);
if (decodedJwt instanceof jwt.TokenExpiredError) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: "로그인 세션이 만료되었습니다. 다시 로그인 해주세요",
});
} else {
const sql = `INSERT INTO cartItems(book_id,quantity,user_id) VALUE(?,?,?)`;
let values = [book_id, quantity, decodedJwt.id];
conn.query(sql, values, (err, results, fields) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
return res.status(StatusCodes.CREATED).json(results);
});
}
};
다음으로 JsonWebTokenError입니다. 이 에러는 토큰이 이상한값일때 에러를 출력합니다. 실제로 토큰에서 이상한값을 넣었더니 아래의 에러가 발생하였습니다.
else-if문으로 아래처럼 해당 오류가 발생했을때 에러만 처리해주면됩니다.
if (decodedJwt instanceof jwt.TokenExpiredError) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: "로그인 세션이 만료되었습니다. 다시 로그인 해주세요",
});
} else if (decodedJwt instanceof jwt.JsonWebTokenError) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: "토큰 값을 확인해주세요",
});
근데 문제가 발생합니다. 아래의 처리를 모든 api함수에서 다해주면 코드가 중복되는 부분이 많아집니다.
'백엔드 > node.js(express)' 카테고리의 다른 글
도서주문관리 프로젝트 - 4.비동기(async-awiat)이용한 쿼리실행(주문) (0) | 2024.01.11 |
---|---|
도서주문관리 프로젝트 - 3. 도서,좋아요 API(SQL시간범위, 페이지네이션,서브쿼리) (1) | 2024.01.11 |
도서주문관리 프로젝트 - 2. 유저API(컨트롤러, 단뱡향암호화(crypto),jwt token) (1) | 2024.01.03 |
도서주문관리 프로젝트 - 1.API 설계(API,테이블) (0) | 2023.12.28 |
JWT (0) | 2023.12.26 |