현재 우리의 서비스는 로그인을 하지 않고 이용이 가능한 api와 로그인을 한 뒤 access_token을 통해 접근이 가능한 api 들이 존재한다.
로그인을 통해 접근이 가능한 api는 access_token이 만료될 시 401 에러가 발생하며 refresh_token을 통해 access_token을 다시 재발급 받아야 재인증이 가능하다.
위의 access_token을 재발급 받는 로직을 구현하기 위해 axios interceptor로 구현을 해보았다
export const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 2000,
});
export async function myAccount(token:string): Promise<Account> {
try {
const response = await instance .get(`/account/myaccount`,{
headers:{
Authorization:`Bearer ${token}`
}
});
return response.data;
} catch (error) {
// axios 에러인지 체크
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
try {
// refresh token으로 새 access token 발급 요청
const refreshResponse = await instance.post<{access_token: string}>(
'/auth/refresh',
{},
{ withCredentials: true }
);
// 새 토큰 저장
const new_access_token = refreshResponse.data.access_token;
localStorage.setItem('access_token', new_access_token);
// 원래 요청 재시도
const retryResponse = await authInstance.get('/account/myaccount', {
headers: {
Authorization: `Bearer ${new_access_token}`
}
});
return retryResponse.data;
} catch (refreshError) {
// refresh token도 만료된 경우
localStorage.removeItem('access_token');
window.location.href = '/'; // 로그인 페이지로 리다이렉트
throw new Error('세션이 만료되었습니다. 다시 로그인해주세요.');
}
}
}
throw error; // 401 이외의 에러는 그대로 throw
}
}
myAccount
외 엄청나게 많이 존재하기에 위의 문제를 개선해야했다.axios 인터셉터는 http 요청이나 응답을 보내기 전/후에 가로채서 처리할 수 있게 해주는 기능이다
axios.interceptors.request.use(
// 요청 보내기 전 실행되는 함수
(config) => {
// 요청 성공 처리
return config;
},
// 요청 에러 시 실행되는 함수
(error) => {
// 요청 에러 처리
return Promise.reject(error);
}
);
// 2. Response 인터셉터
axios.interceptors.response.use(
// 응답을 받은 후 실행되는 함수
(response) => {
// 응답 성공 처리
return response;
},
// 응답 에러 시 실행되는 함수
(error) => {
// 응답 에러 처리
return Promise.reject(error);
}
);
authInstance.interceptors.request.use(
(config) => {
const access_token = localStorage.getItem('access_token');
if (access_token) {
config.headers.Authorization = `Bearer ${access_token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
export const authInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 2000,
withCredentials: true,
});
authInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
try {
const response: AxiosResponse<Login> = await instance.post(
'/auth/refresh',
{
refreshToken: getCookie('refresh_token'),
},
{ withCredentials: true },
);
const new_access_token = response.data.access_token;
const new_refresh_token = response.data.refresh_token;
localStorage.setItem('access_token', new_access_token);
setCookie('refresh_token', new_refresh_token, {
path: '/',
httpOnly: true,
});
originalRequest.headers.Authorization = `Bearer ${new_access_token}`;
return authInstance(originalRequest);
} catch (error) {
localStorage.removeItem('access_token');
removeCookie('refresh_token');
window.location.href = '/';
return Promise.reject(error);
}
}
return Promise.reject(error);
},
);
export async function myAccount(): Promise<Account> {
const response = await authInstance.get(`/account/myaccount`);
return response.data;
}