백엔드 개발자와 회의 하면서, 토큰 전략을 고민했는데 아래와 같이 구현하였다.
accessToken 만료시간: 2시간
refreshToken 만료시간: 2개월
으로 토큰 만료시간 책정
accessToken 만료시 refreahToken 으로 새로운 accessToken을 발급 받는다
이때, refreshToken도 함께 다시 발급받는다
refreshToken의 역할은 2개월동안 새로 접속 했을때 다시 로그인 없이 서비스를 이용할 수 있도록 하는거라 생각했다. 이 이유만으로는 다시 발급 받는 이유가 되지는 않는데, 다시 발급 받는 이유는 accessToken의 만료시간을 2시간으로 잡은 이유와 동일하다고 생각한다.
accessToken의 만료시간을 2시간으로 잡은 이유는 토큰이 탈취 당했다고 하더라도 2시간 후면 accessToken의 효력을 잃어 해커의 접근을 차단 할 수 있다.
그런데 refreshToken을 탈취당하고, 만료시간 2개월이 지나고 새로 발급받는다고 한다면, 해커는 2달동안 accessToken을 새로 발급 받아 마음껏 서비스를 이용할 수 있게 된다.
그렇게 되면 원래 주인인 유저가 접근할때도 가지고 있는 refreshToken으로 새로운 accessToken을 발급 받을 것이고, 해커가 접근할때도 새로운 accessToken을 발급 받게 되는데, 이러면 실제로 계정에 있는 자산을 탈취당하지 않는 이상, 해커가 본인의 계정을 마음껏 이용하고 있는지도 모르게 된다.
그래서 accessToken을 새로 발급받을때마다 refreshToken을 새로 발급받는다.
이렇게 된다면, 해커가 refreshToken을 탈취한다고 해도, 두시간이 지나면 새로운 token을 가지게 되고, 유저의 자동로그인이 풀리거나, 해커가 가지고 있는 token이 무용지물이 된다.
const instance = axios.create();
const _instance = axios.create();
두개를 생성해주는 이유
instance.interceptors.response.use(
...
);
403이라는 응답을 요청한곳에서 받기 전에 가로채는 것이기 때문에 response를 가로챈다
// 정상적인 리스폰스면 그 리스폰스 반환
(response) => {
return response;
},
// 에러 발생 시
async (error) => {
const {
config, // 기존에 요청했던 config가 전달됨
response: { status, message }, // 서버에서 준 에러 메세지
} = error;
if (status === 403) { // 토큰 만료면 토큰 업데이트 요청
const originalRequset = config; // 기존 요청
const refreshToken = sessionStorage.getItem("REFRESH_TOKEN");
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${refreshToken}`,
}; // refresh toekn 으로 요청하기 위해 header를 만듬
try {
const { data } = await _instance({
method: "GET",
url: process.env.REACT_APP_BASEURL + "/token/update",
headers: headers,
}); // interceptor 가 없는 인스턴스로 토큰 업데이트 요청
// 요청 성공시 새로운 토큰 반환
const newAccessToken = data.data.accessToken;
const newRefreshToken = data.data.refreshToken;
// 기존 실패한 요청에서 토큰만 새로운 토큰으로 교체
originalRequset.headers["Authorization"] = `Bearer ${newAccessToken}`;
// 새로 발급 받은 토큰 저장
sessionStorage.setItem("ACCESS_TOKEN", newAccessToken);
sessionStorage.setItem("REFRESH_TOKEN", newRefreshToken);
// 재 요청 갑니다
const res = await _instance(originalRequset);
return res;
} catch (err: any) {
// 만료되서 에러 발생시 로그인 페이지로 이동 시키기
const { status, message } = err.response.data;
if (status === 403) {
alert(message);
window.location.href = "/";
}
}
}
return Promise.reject(error);
}
const instance = axios.create();
const _instance = axios.create();
instance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const {
config,
response: { status, message },
} = error;
if (status === 403) {
const originalRequset = config;
const refreshToken = sessionStorage.getItem("REFRESH_TOKEN");
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${refreshToken}`,
};
try {
const { data } = await _instance({
method: "GET",
url: process.env.REACT_APP_BASEURL + "/token/update",
headers: headers,
});
const newAccessToken = data.data.accessToken;
const newRefreshToken = data.data.refreshToken;
originalRequset.headers["Authorization"] = `Bearer ${newAccessToken}`;
sessionStorage.setItem("ACCESS_TOKEN", newAccessToken);
sessionStorage.setItem("REFRESH_TOKEN", newRefreshToken);
const res = await _instance(originalRequset);
return res;
} catch (err: any) {
const { status, message } = err.response.data;
if (status === 403) {
alert(message);
window.location.href = "/";
}
}
}
return Promise.reject(error);
}
);