레디스 클라우드로 연결은 끝났다 그럼 이제 리프레시 토큰을 발급하고 이것을 레디스에 저장해야 한다.
토큰을 그럼 어떤 경우에 사용할까?
- 당연히 로그인했을 때 처음 발급 된다.
- 로그아웃을 한다면 토큰을 지워줘야 한다.
- 엑세스 토큰이 만료된 경우에 리프레시 토큰을 통해서 다시 재발급한다.
- 이때 액세스 토큰을 발급하는 API를 만들 수도 있지만 매번 새로운 엑세스 토큰을 발급받기 위한 과정이 불필요해 보인다. 한 번에 처리하는 방법이 없을까?
그래서 기존 토큰을 확인하던 미들웨어에서 토큰을 재발급하게 바꿨다.
- 이때 액세스 토큰을 발급하는 API를 만들 수도 있지만 매번 새로운 엑세스 토큰을 발급받기 위한 과정이 불필요해 보인다. 한 번에 처리하는 방법이 없을까?
- 회원 탈퇴를 할 경우에 탈퇴한 회원이기에 접근을 막기 위해 토큰을 지울 필요가 있다.
- 회원 가입을 하고 따로 로그인을 하는 것보다 바로 로그인이 되는 게 더 편할 것 같다.
- 회원가입 완료 시 로그인처럼 토큰 발급해서 로그인 한 유저로 만든다.
- 리프레시 토큰이 탈취되었을 때 액세스 토큰을 계속 찍어낼 수 있는 문제가 생긴다.
- 리프레시 토큰도 일회용으로 만드는 리프레시 토큰 로테이션 전략을 사용했다.
- 앞에 로직들을 모두 그에 맞게 사용한 리프레시 토큰 사용 시 엑세스 토큰과 함께 새로운 리프레시 토큰을 생성 및 레디스 클라우드에 저장
// 리프레시 토큰 저장, 삭제
const redisCli = require('./redisClient');
async function saveRefreshToken(userId, refreshToken) {
try {
const result = await redisCli.set(`refreshToken:${userId}`, refreshToken, 'EX', 7 * 24 * 60 * 60);
if (result !== 'OK') {
throw new Error('리프레시 토큰 저장에 실패했습니다.');
}
return true;
} catch (err) {
console.error('리프레시 토큰 저장 오류:', err);
throw err;
}
}
async function deleteRefreshToken(userId) {
try {
const result = await redisCli.del(`refreshToken:${userId}`);
if (result === 0) {
console.warn(`${userId} 사용자 아이디에 대한 리프레시 토큰이 없습니다. 삭제할 것이 없습니다.`);
return false;
}
return true;
} catch (err) {
console.error('리프레시 토큰 삭제 오류:', err);
throw err;
}
}
module.exports = {
saveRefreshToken,
deleteRefreshToken,
};
// 토큰 확인및 새로운 토큰 발급
const asyncHandler = require("./asyncHandler");
const CustomError = require("./error");
const jwt = require("jsonwebtoken");
const { Users } = require("../models");
const regenerateToken = require("../utils/regenerateToken"); // 새로운 토큰을 발급하는 로직
module.exports = asyncHandler(async (req, res, next) => {
const { MM } = req.cookies;
const [type, token] = (MM ?? "").split(" ");
if (!type || !token || type !== "Bearer") {
throw new CustomError("로그인이 필요한 기능입니다.", 403);
}
try {
const decodeToken = jwt.verify(token, process.env.JWT_SECRET);
const userId = decodeToken.userId;
res.locals.user = userId;
next();
} catch (err) {
// 만약 토큰 검증이 실패했다면, 새로운 토큰을 발급
if (err.name === 'TokenExpiredError' || err.name === 'JsonWebTokenError') {
try {
const newTokens = await regenerateToken(req, res, next);
// 새 토큰을 클라이언트에게 전달하는 로직. 예를 들면 쿠키에 저장.
res.cookie('MM', `Bearer ${newTokens.accessToken}`, {
secure: true,
httpOnly: true,
sameSite: "none",
});
res.cookie('refreshToken', newTokens.refreshToken, {
secure: true,
httpOnly: true,
sameSite: "none",
});
res.locals.user = jwt.verify(newTokens.accessToken, process.env.JWT_SECRET);
next();
} catch (newTokenErr) {
next(newTokenErr);
}
} else {
next(err);
}
}
});
// 토큰 만료 시 새로운 토큰 발급
const jwt = require('jsonwebtoken');
const { saveRefreshToken, deleteRefreshToken } = require('./tokenManager.redis');
const redisCli = require('./redisClient.js');
const CustomError = require("./error");
const util = require('util');
const verifyAsync = util.promisify(jwt.verify); // 비동기적으로 변경
const regenerateToken = async (req, res, next) => {
const clientRefreshToken = req.cookies.refreshToken;
try {
const user = await verifyAsync(clientRefreshToken, process.env.JWT_REFRESH_SECRET);
const storedRefreshToken = await redisCli.get(`refreshToken:${user.userId}`);
if (storedRefreshToken !== clientRefreshToken) {
throw new CustomError("유효하지 않은 리프레시 토큰", 403);
}
const accessToken = jwt.sign(
{ userId: user.userId },
process.env.JWT_SECRET,
{ expiresIn: "15m" });
const refreshToken = jwt.sign(
{ userId: user.userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" });
await deleteRefreshToken(user.userId);
await saveRefreshToken(user.userId, refreshToken);
return { accessToken, refreshToken };
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new CustomError("리프레시 토큰이 만료되었습니다.", 403);
}
next(err);
}
};
module.exports = regenerateToken;
const verifyAsync = util.promisify(jwt.verify); 이렇게 비동기적으로 바꿔준 이유는 에러 처리를 쉽게 하기 위함이다. 기존에 jwt.verify를 사용하면 콜백 기반이기에 에러 처리에 try-catch문을 사용할 수 없다. 그래서 불필요하게 코드가 길어지거나 혹시나 내가 예상하지 못한 문제가 생길 수도 있었다. 그래서 서버에 문제가 생길 위험을 조금이라도 줄이기 위해서 비동기적으로 변경하고 try-catch문을 사용해 에러 처리를 하였다. 그리고 최대한 모듈화를 해서 파일의 수가 많아지더라도 프로젝트 진행 중 변경 사항에 유동적으로 대처할 수 있게 하였다.
'오늘 뭐했냐 > 함께했던 작업들' 카테고리의 다른 글
23.08.26 회원 탈퇴 만들기2 (서버 상태 확인 모듈 만들기) (0) | 2023.09.03 |
---|---|
23.08.25 회원 탈퇴 만들기 1 (기획) (0) | 2023.09.03 |
23.08.22 레디스 클라우드(Redis Cloud) (0) | 2023.08.31 |
23.08.21 프로젝트 중간 점검 (0) | 2023.08.30 |
23.08.10 Error Handling (0) | 2023.08.15 |