ABOUT ME

Today
Yesterday
Total
  • [ Project ] Axios Interceptor 활용하기
    Project 2023. 9. 6. 18:59

    - Axios Interceptor 활용하기

    Axios Interceptor를 활용하여 모든 요청과 응답 전에 원하는 로직을 적용할 수 있다.

    이를 활용하여 

    1. 모든 요청 헤더에 토큰이 필요.

    2. accessToken토큰이 만료되었을 경우 토큰 재발급하고, 갱신된 토큰을 헤더에 설정하고 재요청.

    위 두 가지 문제를 해결할 수 있다.

     

     

    - Request Interceptor, 헤더에 Access Token 설정하기

    const instance = axios.create({
      baseURL: process.env.NEXT_PUBLIC_BASE_URL,
    });
    
    // 요청 인터셉터 : 헤더에 토큰 추가
    instance.interceptors.request.use(
      (config) => {
        const token = {token}
        config.headers.Authorization = token;
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    모든 요청 헤더에 Authorization : token 설정된다.


    - Response Interceptor, 토큰 만료 이슈 처리

    토큰 만료

    헤더에 설정한 토큰이 만료된 토큰이라면, 상태코드 401에러(백엔드에서 설정한 상태코드)가 발생한다.

    응답 Interceptor를 통해서 에러 상태코드에 따른 핸들링을 할 수 있다.

    즉, 토큰 만료에 해당하는 상태코드가 감지되면 토큰 재발급 후에 실패한 요청에 대해서 재요청을 보낼 수 있다.

    정리하자면

    1. 401 응답 감지: 응답 인터셉터에서 401 응답을 감지
    2. 토큰 재발급 요청: 새로운 토큰을 재발급받기 위한 요청
    3. 원래 요청 재시도: 재발급받은 토큰을 사용하여 원래 요청을 재시도. 이때, originalRequest 객체를 수정하여 새로운 토큰을 헤더에 설정
    // 응답 인터셉터: 401 에러 처리
    instance.interceptors.response.use(
      (response: AxiosResponse) => {
        return response;
      },
      async (error: any) => {
        const {config, response: { status }} = error;
        // 토큰 만료 상태코드 401 핸들링
        if (status === 401) {
          const originalRequest = config;
          // refreshToken값 가져오기
          const refreshToken = {refreshToken}
          // 토큰 재발급 요청
         const newToken = await instance.post('api/member/v1/accessToken/expired',{},
              {headers: {refreshToken,},}
            ).then((res) => {return res.data.data})
            .catch((err) => {return Promise.reject(err)});
    		originalRequest.headers.Authorization = newToken
            // 재요청
            return instance(originalRequest);
        }
        return Promise.reject(error);
      }
    );

     

    - 토큰 재발급 무한 루프 

    무한 루프

    토큰 만료된 요청 4개가 각각 토큰 재발급을 받으면서 토큰 만료 <-> 재발급 무한루프가 발생했다.

    대략적으로 상황을 요약하자면

    1. A,B,C,D 요청 모두 토큰이 만료되어 401에러가 발생
    2. A요청에 대한 response interceptor -> 토큰 재발급 -> 토큰 갱신
    3. B요청에 대한 response interceptor -> 토큰 재발급 -> 토큰 갱신
    4. C, D 요청 반복
    5. A 재요청 -> 토큰 만료 401에러
    6. B 재요청 -> 토큰 만료 401에러
    7. C, D 반복
    8. A요청 응답 interceptor 토큰 재발급 -> 토큰 갱신
    9. B요청 응답 interceptor 토큰 재발급 -> 토큰 갱신
    10.  - -무한 반복

    대략 이런 무한 루프인 것 같다.

    무한루프를 해결하기 위해 두 개의 변수를 추가하여 토큰 재발급을 한 번으로 제한하는 로직을 추가했다.

     

    1. 토큰 재발급 중인지 확인하는 플래그 <boolean>  = isRefreshing

    2. 토큰 재발급 요청의 Promise 객체 <Promise> = refreshedTokenPromise

    let isRefreshing = false; // 토큰 재발급 중인지 확인하는 플래그
    let refreshedTokenPromise: Promise<any> | null = null; // 토큰 재발급 요청의 Promise 객체

    - Response Interceptor 401 에러 핸들링 과정

    1. A,B,C,D 요청 모두 토큰이 만료되어 401에러가 발생

    2. A요청에서 최초 401에러를 감지, isRefreshing을 false에서 true로 업데이트.

     // 401 에러 발생하고 토큰 재발급 중이 아닐 때
        if (status === 401 && !isRefreshing) {
          isRefreshing = true; // 재발급 중으로 플래그 변경
          const originalRequest = config;
          // refreshToken 가져오기
          const refreshToken = {refreshToken}
          // 토큰 재발급 요청
          refreshedTokenPromise = axios.post('/api/member/v1/accessToken/expired',{},
              {headers: {refreshToken,},})
            .then((res) => {isRefreshing = false; // 재발급 완료로 플래그 변경
              refreshedTokenPromise = null;
              return res.data.data; // 새로운 토큰 반환
            })
            .catch((err) => {
              isRefreshing = false;
              if (err.response.status === 403) { // refreshToken 만료
                // 로그아웃 로직
              }
              return Promise.reject(err);
            });
          // 새로 발급받은 토큰으로 요청 재실행
          const newToken = await refreshedTokenPromise;
          config.headers.Authorization = newToken.accessToken;
          return instance(originalRequest);

    3. 토큰 재발급 및 refreshedTokenPromise에 토큰 재발급 요청 Promise객체 할당

    4. B,C,D요청에 대한 response interceptor 401에러 핸들링에서는 refreshedTokenPromise이 resolve 상태가 될 때까지 대기

        refreshedTokenPromise 상태가 peding에서 resolve가 되면, 재발급된 토큰으로 재요청

    if (status === 401 && isRefreshing) {
          // 재발급이 완료될 때까지 기다린다.
          const newToken = await refreshedTokenPromise;
          if (newToken) {
            config.headers.Authorization = newToken;
          }
          // 새로운 토큰으로 요청 재실행
          return instance(config);
        }

     

     

    - 전체 코드

    let isRefreshing = false; // 토큰 재발급 중인지 확인하는 플래그
    let refreshedTokenPromise: Promise<any> | null = null; // 토큰 재발급 요청의 Promise 객체
    // * * 코드 생략 
    // 응답 인터셉터: 401 에러 처리 (토큰 만료 등)
    instance.interceptors.response.use(
      (response: AxiosResponse) => {
        // 정상 응답은 그대로 반환
        return response;
      },
      async (error: any) => {
        const {
          config,
          response: { status },
        } = error;
        // 401 에러 발생하고 토큰 재발급 중이 아닐 때
        if (status === 401 && !isRefreshing) {
          isRefreshing = true; // 재발급 중으로 플래그 변경
          const originalRequest = config;
          // refreshToken 가져오기
          const refreshToken = {refreshToken}
          // 토큰 재발급 요청
          refreshedTokenPromise = axios.post('/api/member/v1/accessToken/expired',{},
              {headers: {refreshToken,},})
            .then((res) => {isRefreshing = false; // 재발급 완료로 플래그 변경
              refreshedTokenPromise = null;
              return res.data.data; // 새로운 토큰 반환
            })
            .catch((err) => {
              isRefreshing = false;
              if (err.response.status === 403) { // 재발급 요청에 권한이 없는 경우
                // 로그아웃 로직 추가 필요
              }
              return Promise.reject(err);
            });
          // 새로 발급받은 토큰으로 요청 재실행
          const newToken = await refreshedTokenPromise;
          config.headers.Authorization = newToken.accessToken;
          return instance(originalRequest);
        }
        // 401 에러 발생하고 토큰 재발급 중일 때
        if (status === 401 && isRefreshing) {
          // 재발급이 완료될 때까지 기다린다.
          const newToken = await refreshedTokenPromise;
          if (newToken) {
            config.headers.Authorization = newToken;
          }
          // 새로운 토큰으로 요청 재실행
          return instance(config);
        }
        // 기타 에러는 그대로 반환
        return Promise.reject(error);
      }
    );
    
    export default instance;

     

    무한 루프 해결 👍 토큰 재발급 한 번만 요청된다.

     

Designed by Tistory.