import axios from "axios";
const axiosInstance = axios.create();
let isRefresh = false; // access token을 refresh 했는지 여부
let failedQueue = []; // token 갱신을 대기 중인 요청을 모아두는 배열
// 실패한 요청을 다시 실행하는 함수
function processQueue(error, token = null) {
failedQueue.forEach((fq) => {
if (error) {
fq.reject(error);
} else {
fq.resolve(token);
}
});
failedQueue = [];
}
axiosInstance.interceptors.request.use(
function (config) {
const accessToken = localStorage.getItem("access");
config.headers.Authorization = accessToken;
return config;
},
function (error) {
return error;
}
);
axiosInstance.interceptors.response.use(
// 성공시
function (response) {
return response;
},
// 실패시
function (error) {
const originalRequest = error.response.config;
if (error.response.status === 401 && !originalRequest._retry) {
if (isRefresh) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `JWT ${token}`;
return axios(originalRequest); // 요청 다시 실행
})
.catch((error) => {
return Promise.reject(error);
});
}
if (
localStorage.getItem("access") &&
localStorage.getItem("access") !== originalRequest.headers.Authorization
) {
const accessToken = localStorage.getItem("access");
originalRequest.headers.Authorization = accessToken;
axios(originalRequest);
return;
}
isRefresh = true;
originalRequest._retry = true;
const refreshToken = "refresh_token"
return new Promise((resolve, reject) => {
axios
.post("refresh api 주소", { refresh: refreshToken })
.then(({ data }) => {
window.localStorage.setItem("access", `JWT ${data.access}`);
window.localStorage.setItem("refresh", data.refresh);
originalRequest.headers.Authorization = `JWT ${data.access}`;
processQueue(null, data.access);
resolve(axios(originalRequest)); // 요청 다시 실행
})
.catch((error) => {
processQueue(error, null);
reject(error);
})
.finally(() => {
isRefresh = false;
});
});
}
}
);
export default axiosInstance;
- axiosInstance를 통한 api 요청은 interceptors.request 에서 토큰을 담아서 보낸다.
- api 요청의 결과를 interceptors.response 에서 가로채서 토큰이 만료된 요청(error.response.status === 401) 인지 판별한다.
- 토큰이 만료된 상태에서 들어온 첫 요청(isRefresh가 false)이라면, refresh하여 받은 토큰을 로컬스토리지에 갱신하고, originalRequest 헤더에 갱신한 토큰을 집어넣는다.
⭐️ 여기서 중요한 부분 (흐름을 이해 못해서 한참 걸렸다.) ⭐️
refresh 요청을 보낸시점부터 refresh 요청이 완료되는 시점 사이에 요청을 시도하여 401에러가 발생한 요청들은 4.번 로직에 의해 failedQueue에 차곡차곡 쌓이게 된다.
이후, 토큰 refresh가 성공적으로 완료되면 processQueue 함수에 토큰 넘겨주면서 실패한 요청들을 다시 실행한다. (81 line) (아래 로직으로 실행된다.)
(14 line) failedQueue에 담긴 요청을 하나씩 돌면서 interceptors.response 내의 프로미스 객체의 resolve value 로 token을 넘겨준다.
(47 ~ 50 line) resolve되면 .then으로 넘어온 token을 originalRequest 헤더에 넣어 실패한 요청을 다시 실행한다.
(88 ~ 90 line) finally문에서 요청이 성공했던 실패했던 isRefresh값을 false로 변경해준다. - 토큰이 갱신되고 있는 상태(isRefresh = true) 라면,
(44 ~ 46 line) 토큰을 재발급하지 않고, 에러가 발생한 해당 요청을 프로미스 객체로 반환하여 failedQueue에 넣어놓고 대기한다. (토큰 갱신이 완료되면 차례로 재요청하기 위함) - (56 ~ 65 line) 토큰이 갱신되어 originalRequest 헤더에 들어있는 토큰과 로컬스토리지에 저장된 토큰이 다르다면, (갱신하면서 로컬스토리지에도 다시 저장해주었기 때문)
originalRequest 헤더에 들어있는 토큰을 바꿔치기해서 axios 재요청을 보낸다.
https://stackoverflow.com/questions/57890667/axios-interceptor-refresh-token-for-multiple-requests
Axios interceptor refresh token for multiple requests
I'll throw the http request because I'm calling the refresh token when it returns 401. After the refresh token response, I need to throw the previous request SAMPLE Logın -> — 1 hours later— —> ...
stackoverflow.com
'Frontend > React' 카테고리의 다른 글
[axios] axios error handling (1) | 2023.11.27 |
---|---|
[React] Suspense (0) | 2023.10.31 |
[Webpack + Babel] CRA 없이 리액트 세팅하기 (1) | 2023.10.04 |
[React] React Query 톺아보기 (0) | 2023.07.25 |
[React] Styledcomponents S-dot naming (0) | 2023.07.05 |