JWT과 무엇인지와 사용방법을 익히고 왜 프로젝트에 적용을 해야 하는지 고민해보았다.
세션저장소를 사용 할 수록 서버의 부하가 심해 질 수 있다.
JWT의 state-less(무상태성)
새로운 기술을 알게되어 적용을 해보고싶어서 그냥 적용하기보다는 왜 적용해야하는가에 좀 더 생각해보았다. 물론 단점도 있지만 그 부분을 보완해가면서 진행하면 될 것 같다.
대표적으로 토큰은 노출되어있다. 쿠키나 로컬에 저장을 하게 될텐데, 이는 클라이언트에서 누구나 볼 수 있다는 거다. 여기서 액세스토큰, 리프레쉬토큰 둘 다 저장을 하되 액세스토큰의 유효기간을 매우 짧게 설정 하고, 리프레쉬토큰을 좀 더 안전하게 저장하는 방법을 생각해보면 될 것 같다.
내가 생각한 순서다 아마 토큰의 유효성을 검사하는것을 서버에서 미들웨어로 설정하면 되지 않을까?
내 프로젝트에는 접근성을 위해 카카오로그인만을 사용하고있다. (네이버로그인도 있지만 현재 안쓰고 있고 JWT를 적용 후 다룰 예정)
먼저, 클라이언트와 카카오 사이의 로그인을 진행 후, 완료되었다면 토큰을 발급되게 해주었다.
// root.js
const result = await kakaoAuth.getProfile(access_token);
const user = result;
kakaoUser = {
social_id: { value: user.id, social_name: "카카오 로그인" },
email: user.kakao_account.email,
nickname: user.kakao_account.profile.nickname,
image: user.kakao_account.profile.profile_image_url,
};
const isUser = await User.findOne({ email: kakaoUser.email })
.populate({
path: "on_sale",
populate: { path: "seller_info" },
})
.populate({ path: "chat_rooms", populate: { path: "message_log", populate: { path: "send_user" } } })
.populate({ path: "chat_rooms", populate: { path: "member_list" } })
.populate({ path: "chat_rooms", populate: { path: "product" } });
if (!isUser) {
sendUser = new User(kakaoUser);
await sendUser.save();
} else {
sendUser = isUser;
}
responseData = { success: true, user: sendUser };
if (access_token) {
// 액세스 토큰
const accessToken = jwt.sign(
{
email: sendUser.email,
social_id: { ...sendUser.social_id },
issuer: "ikw-market",
},
process.env.JWT_SECRET_KEY,
{ expiresIn: "1m" }
);
// 리프레쉬 토큰
const refreshToken = jwt.sign(
{
email: sendUser.email,
social_id: { ...sendUser.social_id },
issuer: "ikw-market",
},
process.env.JWT_SECRET_KEY,
{ expiresIn: "24h" }
);
res.cookie("accessToken", accessToken, {
secure: true,
sameSite: "none",
});
res.cookie("refreshToken", refreshToken, {
secure: true,
sameSite: "none",
});
}
return res.status(200).json(responseData);
payload에는 중요한 정보를 담기엔 위험해서 이메일, 필요한정보, 발행자 정도로 넣어줬다. 그러고 응답으로 쿠키에 토큰을 담아 보내게 했음
expiresIn 설정으로 액세스토큰은 1분 리프레쉬토큰은 24시간으로 각각 설정 해두었다.
export const tokenCheck = (req, res, next) => {
const accessToken = req.cookies.accessToken;
const refreshToken = req.cookies.refreshToken;
if (!accessToken) return res.status(401).json({ error: "Unauthorized" });
if (!refreshToken) return res.status(401).json({ error: "Unauthorized" });
// 액세스 토큰 체크하는 함수
const accessTokenCheck = async () => {
try {
jwt.verify(accessToken, process.env.JWT_SECRET_KEY);
return next();
} catch (error) {
// 액세스 토큰 체크 후 리프레시 토큰 체크
await refreshTokenCheck();
}
};
// 리프레시 토큰 체크하는 함수
const refreshTokenCheck = async () => {
try {
// 유효한지 검사
const payload = await jwt.verify(refreshToken, process.env.JWT_SECRET_KEY);
const accessToken = createAccesToken(payload.email);
res.cookie("accessToken", accessToken, {
secure: true,
sameSite: "none",
});
return next();
} catch (error) {
console.error("리프레시 토큰 검사 실패:", error);
return res.status(401).json({ error: "Unauthorized" });
}
};
accessTokenCheck();
};
// 액세스토큰 생성
export const createAccesToken = async (email) => {
const user = await User.findOne({ email })
.populate({
path: "on_sale",
populate: { path: "seller_info" },
})
.populate({ path: "chat_rooms", populate: { path: "message_log", populate: { path: "send_user" } } })
.populate({ path: "chat_rooms", populate: { path: "member_list" } })
.populate({ path: "chat_rooms", populate: { path: "product" } });
if (!user) {
return res.status(400).json({ err: "찾을수 없는 사용자" });
}
const accessToken = await jwt.sign(
{
email: user.email,
social_id: { ...user.social_id },
issuer: "ikw-market",
},
process.env.JWT_SECRET_KEY,
{ expiresIn: "1m" }
);
return accessToken;
};
api 요청이 왔을때 토큰을 검사할 미들웨어를 하나 만들고, 토큰들의 유무, 유효성을 검사 후 재발급 또는 로그인 페이지로 이동하게 처리했다 처음에는 res.redirect를 통해 클라이언트 페이지를 /login 페이지로 이동하게 하려는 멍청한 짓을 했다. 이 부분은 응답으로 status401을 보내고 클라이언트 단에서 401이 뜨면 로그인 페이지로 이동하게 했다.
하고 보니 정말 간단한 작업이였던거 같다 조금 보완해야할 점은 리프레쉬토큰을 좀 더 안전하게 보관하는법과 프로덕션 단계에서의 쿠키 굽기이다 로컬에서는 잘 되지만 배포 시 좀 달라질거 같아서 추후 배포 할 때 다시 작업 해야 할 듯