1. JWT
1.1. 쿠키에 JWT 담아보내기
jwt 토큰에 대한 아주 간단한 api 를 만들었다.
흐름
1. 클라이언트가 서버에 /jwt 로 request 를 보냄
2. 서버가 새로운 토큰 만듦 (JWT 토큰)
3. 만든 토큰을 jwt 라는 이름을 가진 쿠키에 저장하고, reponse 헤더에 Set-Cookie 에 쿠키를 자동으로 포함해서 클라이언트에게 전송
4. 클라이언트는 받은 쿠키를 저장하고, 이후 동일한 도메인에 요청을 보낼 때 쿠키를 자동으로 포함해서 서버로 다시 전송
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 에서 발급한 토큰 값
새롭게 request 를 보냄
핑크색: 이전 request 에서 쿠키에 담아보낸 토큰 == 위에 사진에 있는 값과 같음
노란색: 이번 request 에서 새롭게 생성한 토큰 -> request 에 대한 response 로 쿠키에 담아서 새로 발급한 토큰을 담아서 돌려줌
정리하자면,
-> 클라이언트가 /jwt 로 request 을 보낼 때, 이전 요청에서 서버로부터 받은 쿠키가 Cookie 헤더에 포함되어 서버로 전달됨
-> 서버는 새로운 request 를 처리하면서 새로운 JWT 를 생성하고, 이 값을 다시 쿠키에 저장하여 response 에 담아서 보내줌
※ 추가
매번 토큰을 발급하는 것은 비효율적인 일이라고 생각했기 때문에 웹에서는 보통 어떤 식으로 쿠키를 사용하는지 찾아보았다.
대부분의 경우 실시간 갱신이 필요하지 않고, request 가 올 때마다 새로운 JWT 를 발급하는 것은 자원 낭비이므로 매번 토큰을 발급하지는 않는다고 한다. 또 한 번 발급된 JWT 는 유효기간동안 동일한 토큰으로 사용되며, 클라이언트는 이 토큰을 보유하고 만료되기 전까지 같은 토큰을 서버에 보내는 식으로 사용한다고 한다.
1.2. authorization 받아보기
JWT 는 Header, payload, Signature 이렇게 세 부분으로 구성된는데, jwt.verify() 는 토큰의 마지막 부분인 signature(서명) 가 올바른지 확인한다. 이 signature 를 검증해서 토큰이 위조되지 않았는지 확인할 수 있다. 첫번째 인자로 검증할 토큰을 넣고, 두번째 인자로 토큰을 생성할 때 사용한 비밀키를 넣는다.
app.get('/jwt', function(req, res) {
let token = jwt.sign({ foo: 'bar' }, process.env.PRIVATE_KEY);
res.cookie("jwt", token, {
httpOnly: true
});
console.log(token);
res.send("토큰 발행 완료")
});
app.get('/jwt/decoded', function(req, res) {
let receivedJwt = req.headers["authorization"];
let decoded = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
res.send(decoded);
});
토큰을 받아서 Authorization 필드에 넣어서 request 를 보내면, req.headers["필드명"] 을 사용해서 가져올 authorization 필드에 들어있는 토큰을 가져올 수 있다.
검증이 성공하면, response 로 토큰 안에 들어있는 값을 돌려준다.
2. JWT expire
2.1. TokenExpiredError
-> 토큰의 유효기간이 끝났기 때문에 발생하는 에러
-> 토큰은 토큰을 사용할 수 있는 유효기간이 있는데, 유효 기간이 끝나면 토큰은 더 이상 유효하지 않아서 서버에서 해당 토큰을 인증으로 사용할 수 없게 된다.
-> 이 에러가 나면 500 에러로 표시가 되기 때문에 따로 예외 처리(개발자가 생각하지 못한 에러 처리)를 해서 500에러가 나지 않도록 만들어야 한다. (500 에러가 난다면 무조건 백엔드 책임이기 때문에.. 최대한 나지 않는게 좋다)
2.2. JsonWebTokenError
-> 토큰 자체에 문제가 있음 == JWT 토큰의 구조나 서명이 유효하지 않거나, 토큰 자체가 손상됨
-> jwt.verify() 또는 jwt.decode() 함수로 JWT를 검증할 때 발생
2.2. jwt 예외처리
- TokenExpiredError
토큰을 받아서 검증하는 함수에서 try...catch 문을 사용해서 예외처리를 해줬다.
토큰의 유효기간이 끝난 경우, catch 문으로가서 로그인 세션이 만료되었다는 문구를 클라이언트에게 보낸다.
function ensureAuthorization(req, res) {
try {
let receivedJwt = req.headers["authorization"];
let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
return decodedJwt;
} catch(err) {
console.log(err.name);
console.log(err.message);
return res.status(StatusCodes.UNAUTHORIZED).json({
"message": "로그인 세션이 만료되었습니다. 다시 로그인 하세요."
});
}
};
※ 추가 - ERR_HTTP_HEADERS_SENT
TokenExpiredError -> err.name
jwt expired -> err.message
그리고 그 밑에는 ERR_HTTP_HEADERS_SENT 라는 에러를 던지고 있다. 이 에러는 토큰이 만료된 것과는 상관이 없는 에러인 것 같은데 무슨 에러이고 왜 나는걸까?
결론부터 말하면 리스폰스를 2번 보내서 발생한 에러였다.
토큰 에러가 발생하면,
ensureAuthorization 함수 안에 catch 문에서 return 하면서 클라이언트에게 리스폰스를 보내고, authorization 변수 안에는 검증된 토큰 값이 아닌 전혀 다른 값이 담기게 된다. 그리고 밑으로 내려가서 다른 값이 담긴 authorization 을 인자로 받은 sql 쿼리문이 실행되고 에러가 발생해 BAD_REQUEST 라는 상태코드를 담은 리스폰스가 또 클라이언트에게 보내지기 때문에 에러가 발생한다.
const getCartItems = (req, res) => {
// 장바구니 아이템 목록 조회 & 선택한 장바구니 상품 목록 조회
const {selected} = req.body;
let authorization = ensuerAuthorization(req, res);
let sql = `SELECT cartItems.id, book_id, title, summary, quantity, price
FROM cartItems LEFT JOIN books
ON cartItems.book_id = books.id
WHERE user_id = ?`;
let values = [authorization.id];
if (selected) {
sql += ' AND cartItems.id IN (?)';
values.push(selected);
}
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
res.status(StatusCodes.OK).json(results);
}
)
};
instanseOf 를 사용해서 authorization 에 에러 객체가 들어가면 예외 처리를 해줘서 해결했다.
instanseOf 는 자바스크립트 객체가 특정 생성자 함수나 클래스의 인스턴스인지 확인할 때 사용하는 연산자로 객체의 프로토타입 체인을 따라 올라가면서 해당 생성자 함수의 프로토타입이 객체의 프로토타입 체인에 포함되어 있는지 확인하고, 포함이 되어 있다면 true 를 반환한다.
const getCartItems = (req, res) => {
// 장바구니 아이템 목록 조회 & 선택한 장바구니 상품 목록 조회
const {selected} = req.body;
let authorization = ensuerAuthorization(req, res);
if (authorization instanceof jwt.TokenExpiredError) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요."
})
} else {
let sql = `SELECT cartItems.id, book_id, title, summary, quantity, price
FROM cartItems LEFT JOIN books
ON cartItems.book_id = books.id
WHERE user_id = ?`;
let values = [authorization.id];
if (selected) {
sql += ' AND cartItems.id IN (?)';
values.push(selected);
}
conn.query(sql, values,
(err, results) => {
if (err) {
console.log(err);
return res.status(StatusCodes.BAD_REQUEST).end();
}
res.status(StatusCodes.OK).json(results);
}
)
}
};
- JsonWebTokenError
위의 코드에 else if 문을 추가해서 토큰에 문제(ex. 잘못된 토큰 값 들어옴.. 등등)가 있는 경우도 예외처리를 해주었다.
let authorization = ensuerAuthorization(req, res);
if (authorization instanceof jwt.TokenExpiredError) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: "로그인 세션이 만료되었습니다. 다시 로그인 하세요."
});
}
else if(authorization instanceof jwt.JsonWebTokenError) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: "잘못된 토큰입니다."
});
}
else { ...
}
3. try...catch
개발자가 예상하지 못한 수많은 에러(실수, 사용자가 입력을 잘못함, 디비가 응답을 잘못함.. 등)를 처리하는 문법
try {
// A 코드실행
} catch(err) {
// 에러처리
}
-> try 구문의 코드를 실행하다가 에러가 발생하면 try 코드를 멈추고 catch 로 err 와 함께 빠져나감
-> try 구문에서 어떤 에러가 발생해도, 우리가 다 if 문 분기 처리를 해주던 내용들이 알아서 catch 에 잡힘
(ex. SyntaxError, TypeError ...)
3.1. 에러 객체
자바스크립트 try...catch 문에서 에러 객체는 catch 블록에서 예외가 발생했을 때 그 예외에 대한 정보를 담고 있는 객체로, 발생한 에러에 대한 여러 정보를 제공한다.
에러 객체의 주요 속성으로는 message, name, stack 등이 있다.
- err.message -> 에러 메세지를 담고 있는 문자열로, 에러가 발생한 원인이나 설명을 포함
- err.name -> 에러의 이름으로, 에러 유형을 나타냄 ex) TypeError, ReferenceError, SyntaxError ..
- err.stack -> 에러가 발생한 호출 스택을 포함한 문자열로, 어디에서 에러가 발생했는지 추적할 수 있게 함
'TIL with Programmers' 카테고리의 다른 글
[TIL] 10/21 JS 기초 (2) | 2024.10.22 |
---|---|
[TIL] 10/17 도서 API, 장바구니 API 수정 (feat.JWT) (2) | 2024.10.20 |
[TIL] 10/15 MySQL 데이터 삭제-DELETE/DROP/TRUNCATE, 주문하기 API (0) | 2024.10.15 |
[TIL] 10/14 Node.js 비동기 처리 - Promise, async, await, then, query (0) | 2024.10.14 |
[회고록] 풀 사이클 개발 데브코스 8주차 회고 (5) | 2024.10.12 |