프로젝트 진행중에 jwt를 활용한 로그인 검증 코드를 구현하려는 중에 발생한 문제였다.
jwt 토큰으로 사용자를 검증하고 토큰 유무에 따라 로그인 페이지와 목적 페이지로 리다이렉트 해주는 라우터를 설계해 보았다.
그러나 다른 코드에는 아무 문제가 없었고 에러도 뜨는 것이 없었지만 리다이렉트는 실행되지 않았다. 콘솔도 찍어보고 디버깅도 해봤지만 redirect()
가 실행되지 않는 이유를 알 수 없어서 설 연휴간 계속 고민해보았지만 답을 알 수 없었는데, 연휴가 끝나고 결국 튜터님과 같이 고민하면서 원인을 알게 되었다.
그 원인은 다음 링크를 통해 알게 되어 정리하고자 한다.
https://stackoverflow.com/questions/503093/how-do-i-redirect-to-another-webpage
구체적인 문제가 무엇이었는지 기록해놓기 위해서 코드를 살펴보겠다. 먼저 이벤트 발생 코드이다.
// 4. 리뷰 조회 버튼 클릭시
const getReviewsBtn = document.querySelector('#get-reviews-btn');
getReviewsBtn.addEventListener('click', function () {
verifyTokenAndMovePage('/api/storeId/reviewId');
});
function verifyTokenAndMovePage(URL) {
console.log(URL);
$.ajax({
type: 'GET',
url: '/api/users/me',
data: {},
success: function (response) {
console.log(response.result, response.message);
location.href = URL;
},
});
}
페이지를 이동할 때, 토큰을 검증할 수 있도록 프론트에서 서버의 미들웨어로 연결하는 코드이다.
서버에서 위의 ajax 통신을 받는 미들웨어 코드는 다음과 같다.
const express = require('express');
const jwt = require('jsonwebtoken');
// 페이지 이동 후 GET 요청시 실행되는 미들웨어
// Refresh Token과 Access Token을 검증하는 API
module.exports = async (req, res, next) => {
console.log('/user/me 라우터 실행');
const accessToken = req.cookies.accessToken;
const refreshToken = req.cookies.refreshToken;
// accessToken 없을 경우
if (!accessToken) {
console.log('accessToken 없을 경우');
return res.redirect('/api/login');
}
const isAccessTokenValidate = validate(accessToken);
const isRefreshTokenValidate = validate(refreshToken);
// accessToken 만료시 refreshToken 통해서 재발급
if (!isAccessTokenValidate) {
console.log('accessToken 만료시 refreshToken 통해서 재발급');
// refreshToken 없을 경우 -> 로그인 통한 토큰 재발급 유도
if (!refreshToken) {
console.log(
'refreshToken 없을 경우 -> 로그인 통한 토큰 재발급 유도'
);
return res.redirect('/api/login');
}
// refreshToken 만료시 -> 재로그인 통한 토큰 재발급 유도
if (!isRefreshTokenValidate) {
console.log(
'refreshToken 만료시 -> 재로그인 통한 토큰 재발급 유도'
);
return res.redirect('/api/login');
}
// refreshToken 존재 & 유효 -> 새로운 accessToken 재발급
console.log('accessToken 재발급 전, tokenObject 확인');
console.log(tokenObject);
const accessTokenPayloadData = tokenObject[refreshToken];
const newAccessToken = createAccessToken(accessTokenPayloadData);
res.cookie('accessToken', newAccessToken);
return res.status(201).json({
result: 'success',
message: 'Access Token을 새롭게 발급하였습니다.',
accessTokenPayloadData,
});
}
// accessToken 있을 경우
const accessTokenPayloadData = getAccessTokenPayload(accessToken);
console.log('accessToken 있을 경우');
return res.status(200).json({
result: 'success',
message: 'accessToken 인증 성공',
accessTokenPayloadData,
});
};
// accessToken과 refreshToken을 생성한 후, 쿠키에 저장합니다.
function issueTokens(accessTokenPayloadObject) {
const accessToken = createAccessToken(accessTokenPayloadObject);
const refreshToken = createRefreshToken();
tokenObject[refreshToken] = accessTokenPayloadObject; // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
return { accessToken, refreshToken };
}
// Access Token을 생성합니다.
function createAccessToken(id, loginType, themee) {
const accessToken = jwt.sign(
{ id }, // JWT 데이터
JWT_SECRET_KEY, // 비밀키
{ expiresIn: '10s' }
); // Access Token이 10초 뒤에 만료되도록 설정합니다.
return accessToken;
}
// Refresh Token을 생성합니다.
function createRefreshToken() {
const refreshToken = jwt.sign(
{}, // JWT 데이터
JWT_SECRET_KEY, // 비밀키
{ expiresIn: '30s' }
); // Refresh Token이 30초 뒤에 만료되도록 설정합니다.
return refreshToken;
}
// Refresh Token이나 Access Token을 검증합니다.
function validate(Token) {
try {
jwt.verify(Token, JWT_SECRET_KEY)
return true;
} catch (error) {
return false;
}
}
// Access Token의 Payload를 가져옵니다.
function getAccessTokenPayload(accessToken) {
try {
const payload = jwt.verify(accessToken, JWT_SECRET_KEY); // JWT에서 Payload를 가져옵니다.
return payload;
} catch (error) {
return null;
}
}
이미 accessToken과 refreshToken이 발급된 상태에서 둘 다 만료된 상태에서 코드를 실행했다. 내가 설계한 의도대로라면
- 토큰이 만료된 프론트에서 토큰 검증 요청을 서버에 준다.
- 서버에서 토큰 검증 코드를 통해서 토큰이 만료되었을 경우의 코드를 실행한다.
해당 코드는 위의 코드에서 다음과 같다.
// refreshToken 만료시 -> 재로그인 통한 토큰 재발급 유도 if (!isRefreshTokenValidate) { console.log('refreshToken 만료시 -> 재로그인 통한 토큰 재발급 유도'); return res.redirect('/api/login'); }
- 결국 재로그인을 유도하기 위해서
redirect('/api/login')
을 통해 로그인 창으로 리다이렉트 해주는 것이다.
그런데 아무리해도 리다이렉팅이 되지 않았다. 바로 윗줄의 콘솔까지 찍히는데 리다이렉트는 실행되지 않았고, 프론트에서는 success일 때 location.href = URL
한 것이 실행되어 버리고 말았다.
이 문제의 원인과 해결방안은 사실 상당히 간단했다. 위에 스택오버플로우 링크에 있는 설명에 의하면
window.location.replace(...)
is better than usingwindow.location.href
, becausereplace()
does not keep the originating page in the session history, meaning the user won't get stuck in a never-ending back-button fiasco.
redirect()와 같은 기능을 가진 replace()에 대해서 설명하는데, 결국 사용자가 원하지 않는 무한 로그인, 뒤로가기 루프 상태에 빠지지 않도록 한 설계였다.
작동되지 않는 것이 내가 코드를 잘못 짰다기보다는 초능력의 기능자체가 매우 훌륭했다.
그러면 어떻게 다른 페이지로 redirect() 하는 것이 좋은 방법일까? 위의 스택오버플로우 설명에서는 다음과 같이 설명한다.
If you want to simulate someone clicking on a link, use
location.href
If you want to simulate an HTTP redirect, use
location.replace
결국 프론트에서 서버의 응답 요청을 받은 후, 응답 메시지 등을 따라서 if
문으로 나눠서 처리하는 것이 현실적으로 보인다.