[JWT] Client Side 에서 refresh Token 다중 요청

cgoing·2023년 6월 5일
26
post-thumbnail
post-custom-banner

소개

원본글

안녕하세요! 🤪 이번 포스트에서는 클라이언트 사이드에서 다중 Refresh Token 요청을 처리하는 방법에 대해 이야기하고자 합니다.

JWT 로 인증과 인가를 구현하게 되면 RefreshToekn필수 적으로 해야한다고 생각 합니다.

AccessToken 만 사용하게 되면 토큰탈취를 서버에서 알 수 없기 때문이죠.
Refresh Token에 대한 자세한 내용은 다음 기회에 작성하겠습니다.

시작 하기 전에 🤚🏿

JWT 인증과 인가, Promise 에 대한 사전 지식이 필요합니다!

Refresh Token 으로 AccessToken 발급 요청

액세스 토큰이 만료되었을 때, 리프레쉬 토큰으로 억세스 토큰을 재발급 받기 위해 ,
재발급 요청 api 를 쏘게 됩니다.

보통 이 과정은 사용하는 fetcher ( axios ) 에 Proxy 혹은 Interceptor 에 달게 됩니다.


const authInterceptor = (instance: AxiosInstance) => {
  

  instance.interceptors.request.use(async (config) => {
	// 요청할때 token 을 같이 보냄 
    config.headers.Authorization = `Bearer ${getAccessToken()}` // store, storage, 아니면 쿠키로 전송 
    return config;
  });
  instance.interceptors.response.use(
    (response) => {
      return Promise.resolve(response);
    },
    async (axiosError) => {
      const {
        response: { data: error, status },
        config,
      } = axiosError;

      if (status == 401) {
        if (error?.message == "AccessTokenExpiredError") {
	 	//  토큰 만료 에러가 오면  refreshToken 으로  accessToken 재발급 요청 
          await instance
          .get("/auth/refresh",{refreshToken: getRefreshToken()})
          return instance(config); // 기존 요청을 재요청
          
        }
      }

      return Promise.reject(axiosError);
    }
  );
};

이렇게 보통 AccessToken 이 만료가 되면 refreshToken 으로 재발급 요청하는 로직을 작성하게 됩니다.

😱 문제가 있습니다.

refreshToken 은 서버에 저장이 되고, one time use 로 한번 사용한 refreshToken 은 폐기 되고 accesToken 과 같이 다시 생성 되기도 합니다. ( 요구 사항에 따라 다름 )

문제는 일반적으로 특정 페이지를 진입하며 Componenet 가 Mounted 되는 시점에서 불러오는 API는 여러개 일 수 있습니다.

예를들어, 의류 상품에 디테일 페이지에 접속하게 되고, queryString 통해 받은 특정 의류의 Id 로
서버에 디테일, 평점, 비슷한 상품 등 따로따로 API 를 호출 한다고 가정해 본다면,
이 3개의 API 는 동시에 요청하게 됩니다.

하필 AccessToken 이 만료 된 시점 이라면, 3개의 API 에서 전부 Accesstoken 만료에 대한 상태를 response 하게되고, 이어서 3번의 RefreshToken 으로 재발급 요청을 하게 됩니다.

그렇다면, 실제론 AccessToken 은 3개가 발급이되고, refreshToken도 3번이나 update 되게 됩니다.
물론, Client Side 에서는 마지막에 온 Token 으로 등록이 되지만, 불필요한 요청과 순서보장도 없어집니다.

이 문제를 해결하기 위해 Promise 를 관리하고 resolve,reject 를 직접 정하는 Holder 가 필요 합니다.

🔗 Holder 🔗

export class Holder<T> {
  promise: Promise<T>;
  resolve: Function;
  reject: Function;
  constructor() {
    this.hold();
  }
  hold() {
    this.promise = new Promise((resolve, reject) =>
      Object.assign(this, { reject, resolve })
    );
  }
}

Holder 는 다른 코드블럭, 함수 를 async 하게 controll 할 수 있는 Class 입니다.
TMI 로 저는 개인적으로 js 에서 가장 좋아하는 객채는 Promise, Proxy 입니다.

바로 개발자도구를 키셔서 해당 클래스를 사용해 볼수 있습니다.

class Holder {
  promise;
  resolve;
  reject;
  constructor() {
    this.hold();
  }
  hold() {
    this.promise = new Promise((resolve, reject) =>
      Object.assign(this, { reject, resolve })
    );
  }
}

const holder = new Holder()

const a = async ()=>{
	console.log(`a start`);	
	await holder.promise;
	console.log(`a end`);	
}

const b = async ()=>{
	console.log(`b start`);	
	await holder.promise;
	console.log(`b end`);	
}


a(); //  'a start' holder 에 promise 가 Pendding 상태 
b(); //  'b start' holder 에 promise 가 Pendding 상태  

holder.resolve();
//  holder의 promise 가 fulfilled 되면서 'a end', 'b end' 출력 

a(); // 'a start a end '   이미 holder 가 fulfilled 이기 때문에 

정말 유용하게 사용되는 클래스입니다.

👆🏻AccessToken 재발급 요청은 딱 한번만~

export const authInterceptors = (instance: AxiosInstance) => {
  let lock = false;
  const holder = new Holder();

  instance.interceptors.request.use(async (config) => {
    if (lock && config.url !== "/auth/refresh") await holder.promise; // accessToken 재발급 요청 중에는  다른 요청을 잠시 hold 함 
    return config;
  });
  instance.interceptors.response.use(
    (response) => {
      return Promise.resolve(response);
    },
    async (axiosError) => {
      const {
        response: { data: error, status },
        config,
      } = axiosError;

      if (status == 401) {
        if (error?.message == "AccessTokenExpiredError") {
          try {
            if (!lock) {
              lock = true;
              holder.hold();

              await instance
                .get("/auth/refresh")
                .then(() => {
                  lock = false;
                  holder.resolve();
                })
                .catch(() => {
                  lock = false;
                  holder.reject();
                  throw new Error();
                });
            } else await holder.promise; // 이미 accessToken 재발급 요청을 했다면  hold 

            return instance(config); // 기존 요청을 재요청
          } catch (error) {
            error;
          }
        }
      }

      return Promise.reject(axiosError);
    }
  );
};

3개의 토큰 만료 응답이 도착하게 된다면,
lock 을 통해서 AccessToken 재발급 요청에 대한 Flag 를 저장해서, 제일 먼저 도착한
만료 응답 API 만, 재발급 요청을 하게 됩니다. 나머지 2개, 혹은 , 혹여나 다른 요청을 하는 API 가 있다면, 재발급의 응답을 다같이 기다려 주게 됩니다.

javascript 의 비동기 관리는 항상 너무 재밌습니다. 🤣

긴 글 읽어 주셔서 감사합니다.

profile
사랑해
post-custom-banner

9개의 댓글

comment-user-thumbnail
2023년 6월 13일

Very nice post. Keep sharing this kind of valuable post. A really great stuff posted here.
MCDVOICE

1개의 답글
comment-user-thumbnail
2023년 7월 4일

너무 귀여운 캐릭터 사진! Vampire Survivors

답글 달기
comment-user-thumbnail
2023년 8월 8일

I wanted to acknowledge your great post—it's truly appreciated. Your insights have provided valuable perspectives. For those interested in exploring something unique, I encourage you to visit [url=https://sellfeetpics.vip/]How to earn through feet pics[/url] and discover intriguing opportunities on my website. Your engagement is highly valued, and your curiosity in uncovering new possibilities is commendable.

답글 달기
comment-user-thumbnail
2023년 9월 5일

Thanks for sharing such a useful and helpful blog. Keep doing your best.
https://mykohlscards.online/my-kohls-credit-card-payment/

답글 달기
comment-user-thumbnail
2023년 10월 24일

I found the most beautiful and fascinating one. https://www.jacketsexpert.com/product/givenchy-letterman-jacket/

답글 달기
comment-user-thumbnail
2023년 10월 27일

Excellent blog! Such clever work and exposure! Keep up the very good work. https://www.jacketscreator.com/product/top-boy-jamie-parachute-jacket/

답글 달기
comment-user-thumbnail
2023년 12월 8일

It's actually a great and helpful piece of information. I am satisfied that you just shared this useful information for us. https://www.jacketsmasters.com/product/barbie-margot-robbie-vest/

답글 달기
comment-user-thumbnail
2024년 7월 12일
답글 달기