카테고리 없음

401시 토큰 재발급

LSH2118 2025. 5. 17. 13:36

이번에 프로젝트를 진행하면서 로그인, 회원가입 기능을 맡게 됐다

 

클라이언트가 요청을 보낼 때 accessToken을 같이 보내게 되는데, 토큰이 없거나 만료되면,

서버에서 상태 코드 401을 반환한다

 

그래서 상태코드가 401이고, refreshToken이 있으면 토큰을 재발급을 받는 코드를 작성했는데

 

막상 코드를 재발급을 받은 후에 재요청이 가지 않았다

 

해결방법

우리 프로젝트에선 Axios Interceptors를 사용하고 있는데,

 

이 asxios interceptors가 뭐냐면 쉽게 말해 요청 전후의 데이터를 가로채서 우리 마음대로 관리하는 역할을 한다

 

나는 Interceptors안에서 재발급코드와, 재요청 로직을 작성했다

let isRefreshing = false; // 지금 토큰을 재발급 받고 있는지 확인하는 변수
let refreshSubscribers: ((token: string) => void)[] = []; 
// 토큰 재발급을 받고 있을때 들어가는 요청 대기열

const onTokenRefreshed = (token: string) => {
  refreshSubscribers.forEach((callback) => callback(token));
  refreshSubscribers = [];
};// 새로운 accessToken이 왔을때 요청 대기열 안에 있는 요청들을 순서대로 다시 요청하는 함수

const addRefreshSubscriber = (callback: (token: string) => void) => {
  refreshSubscribers.push(callback);
};// 토큰 재발급 받고 있는 중인데 요청이 들어오면 요청을 요청 대기열에 추가하는 함수

axiosInstance.interceptors.response.use(
  (response) => {
    return response.data;
  }, // response가 성공일때
  async (error) => { // response가 실패일때
    const originalRequest = error.config; // 현재의 axios 요청을 가져온다

    if (error.response?.status === 401 && !originalRequest._retry) { // 만약 상태코드가 401이고, _retry가 false면 실행
      originalRequest._retry = true; // 요청 객체에 임의로 _retry라는 값을 추가, true

      const refreshToken = getCookie('refreshToken'); // refreshToken을 가져온다

      if (!refreshToken) { // 리프레시토큰도 없으면 그냥 반환
        return Promise.reject(error);
      }

      if (isRefreshing) { // 현재 토큰 재발급 중이라면, 요청을 대기열에 추가
        return new Promise((resolve) => {
          addRefreshSubscriber((token: string) => {
            originalRequest.headers.Authorization = `Bearer ${token}`;
            resolve(axiosInstance(originalRequest));
          });
        });
      }

      isRefreshing = true; // 재발급 상태를 true로 변경

      try { // 토큰 재발급 try catch
        const response: RefreshResponse = await axiosInstance.post(
          '/auth/refresh',
          {
            refreshToken,
          }
        );
		
        //응답으로 온 쿠키 최신화
        document.cookie = `accessToken=${response.accessToken}; path=/;`; 
        document.cookie = `refreshToken=${response.refreshToken}; path=/;`;

        onTokenRefreshed(response.accessToken); // 요청대기열에 토큰 넣어서 재요청

        originalRequest.headers.Authorization = `Bearer ${response.accessToken}`; // 원래 요청 헤더에 토큰 추가

        return axiosInstance(originalRequest); // 재요청
      } catch (error) {
        console.error(error);
      } finally { // 토큰 재발급이 성공했든 안했든 끝났으니 false화
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

 

위 코드에 대한 흐름을 설명하자면

 

1. 먼저 실패된 요청이 이 코드 안으로 들어온다

2. 상태코드가 401이고, _retry가 애초에 존재하지 않으니 if조건에 걸려서 안으로 들어간다

3. 그다음에 요청에 임의로 _retry라는 값을 true로 추가한다

4. refresh토큰이 있으면 재발급 코드가 실행

5. 성공적으로 재발급이 됐으면 원래 실패했던 요청을 재요청

 

그럼 위 흐름에서 사용되지 않았던 코드나, isRefreshing, _retry의 역할은 뭐냐?

 

이것들은 무한루프 방지와 재발급 도중 들어온 요청들을 위해 사용된다

 

만약 우리가 보낸 요청이 401이 발생해서 이 interceptors로 들어간다

그리고 refresh요청을 보내는데 거기서도 401이 발생, 

다시 interceptors로 들어가는 무한루프가 대표적이다

 

그리고 만약 토큰을 재발급을 받고 있는데 다른 401이 들어와서 다시 토큰 재발급 요청을 보내면

토큰이 중복 발급되는 경우가 생긴다

 

이는 서버에도 좋지 않고, 또 다른 무한 루프를 초래할 수 있다

 

그래서 일종의 큐(queue)를 만들어 관리한다

 

1. 위에서 설명한 흐름대로 에러를 처리하는 과정 중 새로운 401 요청이 interceptors안으로 들어옴

2. 또 위에서 설명한 흐름대로 처리되다가 딱 isRefreshing에서 막힌다

 if (isRefreshing) { // 현재 토큰 재발급 중이라면, 요청을 대기열에 추가
    return new Promise((resolve) => {
      addRefreshSubscriber((token: string) => {
        originalRequest.headers.Authorization = `Bearer ${token}`;
        resolve(axiosInstance(originalRequest));
      });
    });
}

isRefreshing이 true이니 위 코드가 실행된다

 

먼저 Promise를 반환해서 기다리게 만들고,
addRefreshSubscriber라는 대기열 큐에 함수를 추가한다

const addRefreshSubscriber = (callback: (token: string) => void) => {
  refreshSubscribers.push(callback);
};

 

함수 내용은 토큰이 들어오면 요청 헤더에 새 토큰을 추가하고 resolve를 사용해서 재요청을 보내는 기능을 수행한다

 

3. 그러고 기다리고 있다가 첫 번째 요청에서 재발급된 새 토큰이 들어오면 onTokenRefreshed함수에 토큰을 넣어서 refreshSubscribers안에 대기 중이던 요청에 token을 넣는다

const onTokenRefreshed = (token: string) => {
  refreshSubscribers.forEach((callback) => callback(token));
  refreshSubscribers = [];
};

 

그러면 refreshSubscribers안에 있던 요청들이 차례대로 재요청이 간다

 

그리고 전부 요청을 끝냈으면 refreshSubscribers를 초기화시킨다

결론

우선 저 코드를 이해할 때에 굉장히 놀랐었다

함수 자체를 배열에 콜백으로 넣어서 나중에 들어온 토큰을 넣어 재요청 보낸다는 발상이 가장 새롭고 신기했던 것 같다

 

 다음에도 자체로그인을 구현할 일이 오면 해당 코드를 많이 참고할 것 같다

출처

[Axios] 토큰 재발급 중복 요청 문제 해결