산악부 사이트를 배포했는데 문제가 생겼다.
현재 로직에서 로그인을 하면 accessToken과 accessToken의 만료시간을 프론트엔드에 전달하고, 쿠키에 refreshToken을 저장한다.
accessToken의 만료시간이 현재 시간보다 과거가 되는 경우(accessToken이 만료된 경우) 쿠키의 refreshToken을 통해 백엔드에 토큰 재발급을 요청한다.
문제는 백엔드 코드에서 프론트엔드의 refreshToken을 받지 못하는 것이었고,
브라우저의 쿠키를 보았을 때 refreshToken이 존재하지 않았다.
현재 백엔드 코드는 아래와 같다.
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req, @Res() res: Response) {
const refreshTokenExpirestime = await this.authService.getTokenExpiresTime(
req.user.refreshToken,
);
res.cookie('refreshToken', req.user.refreshToken, {
httpOnly: true,
maxAge: refreshTokenExpirestime * 1000,
});
const getAccessTokenExpiredTime =
await this.authService.getTokenExpiresTime(req.user.accessToken);
let date = new Date();
date.setSeconds(date.getSeconds() + getAccessTokenExpiredTime);
delete req.user.refreshToken;
return res.json({
...req.user,
expiredTime: date,
});
문제는 현재 프론트엔드 주소와 백엔드 주소가 달라 cookie를 설정해주지 못하는 중이었다.
(쿠키 설정 변경)
이렇게 해서 쿠키를 바꿔주니 정상적으로 쿠키를 브라우저에 저장하고 서버에 받아올 수 있었다.
하지만 문제가 끝나진 않았다 ㅜ
현재 프론트엔드의 토큰 재발급 로직은 다음과 같다.
useEffect(() => {
const now = new Date();
const expiredDate = new Date(expiredTime);
console.log("expiredTime: ", expiredDate);
console.log("now: ", now);
console.log("expiredTime이 현재 시간보다 과거인가? ", expiredDate <= now);
if (expiredTime !== "" && expiredDate <= now) {
console.log(
"expiredTime이 현재 시간보다 과거입니다. accessToken 갱신 시도 중..."
);
axios
.get("/auth/token/access", {
withCredentials: true,
})
.then((res) => {
console.log(res);
console.log("새로운 accessToken을 성공적으로 받았습니다!");
localStorage.setItem("accessToken", res.data.accessToken);
userInfoStore.setState({ expiredTime: res.data.expiredTime });
})
.catch((error) => {
console.error("accessToken 갱신 실패: ", error);
localStorage.removeItem("accessToken");
clearUserInfoStorage();
basicAxios.post("/auth/logout");
window.location.href = "/login";
});
console.log(
"accessToken이 유효하지 않습니다. 로그인 페이지로 이동합니다."
);
} else {
console.log("accessToken이 유효합니다.");
}
}, [expiredTime]);
(응답이 잘 가는지 확인하기 위해 console.log를 찍어봄)
여기서 문제는 서버엔 요청이 잘 가고 서버에서 응답을 보내준다.
문제는 클라이언트에서 재발급 받은 토큰을 저장하지 못하고 있었다.
문제는 백엔드에서 보내주는 형태가 이상했다.
원래의 백엔드 코드는 다음과 같다.
@Get('token/access')
async getAccessToken(@Request() req: ExpressRequest, @Res() res: Response) {
const refreshToken = req.cookies['refreshToken'];
if (!refreshToken) {
res.clearCookie('refreshToken', {
httpOnly: true,
path: '/',
sameSite: 'none',
secure: true,
});
throw new UnauthorizedException('로그인이 필요합니다.');
}
const { accessToken: newAccessToken } =
await this.authService.rotateToken(refreshToken);
return newAccessToken;
}
위 응답은 Promise<string>으로 간다.
문제는 이렇게 응답을 주면 프론트엔드에서 응답값을 받지 못한다.
찾아본 결과!!
@Res 데코레이터를 사용하는 경우 , 개발자가 응답을 직접 제어해야 한다.그렇지 않은 경우 응답이 클라이언트에게 전송되지 않는다.
https://docs.nestjs.com/controllers
난 지금까지 응답을 주고있지 않던 것이었다 ;ㅡ;
@Get('token/access')
async getAccessToken(@Request() req: ExpressRequest, @Res() res: Response) {
const refreshToken = req.cookies['refreshToken'];
if (!refreshToken) {
res.clearCookie('refreshToken', {
httpOnly: true,
path: '/',
sameSite: 'none',
secure: true,
});
throw new UnauthorizedException('로그인이 필요합니다.');
}
const { accessToken: newAccessToken } =
await this.authService.rotateToken(refreshToken);
const getAccessTokenExpiredTime =
await this.authService.getTokenExpiresTime(newAccessToken);
let date = new Date();
date.setSeconds(date.getSeconds() + getAccessTokenExpiredTime);
return res.json({
accessToken: newAccessToken,
expiredTime: date,
});
}
위 형식으로 응답을 수정하니 프론트엔드에서 값을 받을 수 있게 되었다.
useEffect(() => {
const now = new Date();
const expiredDate = new Date(expiredTime);
const timeDiffBetweenNowAndExpiredTime = (expiredDate - now) / 1000; // accessToken 만료까지 남은 시간
if (expiredTime !== "" && timeDiffBetweenNowAndExpiredTime <= 30) {
axios
.get("https://dkuac.store/auth/token/access", {
withCredentials: true,
})
.then((res) => {
localStorage.setItem("accessToken", res.data.accessToken);
userInfoStore.setState({ expiredTime: res.data.expiredTime });
})
.catch((error) => {
console.error("accessToken 갱신 실패: ", error);
localStorage.removeItem("accessToken");
clearUserInfoStorage();
basicAxios.post("/auth/logout");
window.location.href = "/login";
});
}
}, [expiredTime]);
그 후 리액트의 로그인 관리 부분을 위와 같이 바꿨다.
먼저 accessToken의 만료시간을 체크한다.
만료시간이 30초 이하가 되면, 백엔드에 토큰 재발급 요청을 보낸다.
그 후 재발급 된 토큰과 새로 받은 만료시간을 저장한다.
이제 토큰과 만료시간이 잘 저장된다~! ^ㅡ^