
// index.js
import axios from "axios";
axios.defaults.withCredentials = true;
axios.defaults.baseURL = process.env.REACT_APP_SERVER_URL;
// 로그인 기능
const onValidSubmit = useCallback(async (data: ILogin) => {
const {
data: { accessToken },
status,
} = await logIn(data);
if (status !== 201) return alert("로그인 실패");
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
}, []);
// axios.ts
import { getRefreshToken } from "./cookie";
let isTokenRefreshing = false;
let refreshSubscribers: Array<(accessToken: string) => void> = [];
const addRefreshSubscriber = (callback: (accessToken: string) => void) => {
refreshSubscribers.push(callback);
};
const onTokenRefreshed = (accessToken: string) => {
refreshSubscribers.map((callback) => callback(accessToken));
};
axios.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const {
config,
response: { status },
} = error;
const originalRequest = config;
if (status === 401) { // 에러 내용 확인도 추가
if (!isTokenRefreshing) {
// isTokenRefreshing이 false인 경우에만 token refresh 요청
isTokenRefreshing = true;
const refreshToken = getRefreshToken();
const { data } = await axios.post(
`${process.env.REACT_APP_SERVER_URL}auth/refreshAccessToken`, // token refresh api
{
refreshToken,
}
);
// 새로운 토큰 저장
const { accessToken: newAccessToken } = data;
isTokenRefreshing = false;
axios.defaults.headers.common.Authorization = `Bearer ${newAccessToken}`;
// 새로운 토큰으로 지연되었던 요청 진행
onTokenRefreshed(newAccessToken);
}
// token이 재발급 되는 동안의 요청은 refreshSubscribers에 추가
const retryOriginalRequest = new Promise((resolve) => {
addRefreshSubscriber((accessToken: string) => {
// 갱신한 accessToken으로 재설정
originalRequest.headers.Authorization = "Bearer " + accessToken;
// originalRequest를 다시 실행
resolve(axios(originalRequest));
});
});
return retryOriginalRequest;
}
return Promise.reject(error);
}
);
export default instance;
이 방법으로 구현을 한다면 엑세스 토큰이 만료되었을 때 한 페이지에서 n만큼 요청을 보내는 상황에서 n번의 엑세스 토큰을 재발급하는 것이 아닌 한 번의 재발급으로 실패한 요청들을 한꺼번에 처리할 수 있다.
interceptor 설명
- 응답 에러가 발생하였을 때 그것이 인증에러라면 interceptor 안에 내용을 실행한다.
isTokenRefreshing이false일 경우에는 토큰 재발급을 실행.
- 재발급 로직을 실행할 때
isTokenRefreshing값을true로 변경.- 토큰 재발급이 완료되면 다시
false로 변경.- axios의 헤더 디폴트 값으로 재발급 받은 엑세스 토큰 값을 설정
onTokenRefreshed(재발급 엑세스 토큰)을 실행.
- 배열에 담긴 실패 요청들을 map()을 통해 재요청 보내는 함수.
isTokenRefreshing이true. 즉, 재발급이 진행 중일 때는retryOriginalRequest를 통해 새로운 엑세스 토큰을 인자로 받아 재요청 보내는 함수를addRefreshSubscriber에 전달.
addRefreshSubscriber는 배열 안에callback(accessToken)들을refreshSubscribers로 push()하는 함수.
요약
응답 에러가 발생하면 첫 번째 요청에 한해서 토큰 재발급을 실행하고 실패한 요청들을 배열에 담아 재발급이 완료되었을 때 한꺼번에 재요청을 실행한다.
개선사항
현재 isTokenRefreshing 이라는 boolean 값으로 첫 번째 요청이 아닌 것들은 재발급을 시키지 않고 있는데 callback 함수를 담은 배열 refreshSubscribers의 길이로 제어할 수 있을 것 같다는 조언이 있었다.
[현재]
if(!isTokenRefreshing){
isTokenRefreshing = true;
// 엑세스 토큰 재발급
isTokenRefreshing = false;
}
// 실패요청 배열에 담기
[개선]
// 실패요청 배열에 담기
if(배열의 길이 <= 1){
// 엑세스 토큰 재발급
}
이번 경험을 통해 Cookie / Storage와 보안에 관련된 지식들을 많이 알게되었다. 로그인 지금까지 쉽게 생각하고 있었는데 정말 엉망진창으로 만들었던거였구나… 아직 서버와 맞춰보지는 않았지만 될 것만 같은 너낌 😁
참고 블로그