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

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

소개

원본글

안녕하세요! 🤪 이번 포스트에서는 클라이언트 사이드에서 다중 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
사랑해

12개의 댓글

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일
답글 달기
comment-user-thumbnail
2025년 2월 4일

Great information about wilderness for beginners giving the opportunity for new people. https://www.jacketmakers.com/product/viva-la-vida-chris-martin-jacket/

답글 달기
comment-user-thumbnail
2025년 2월 19일

Fantastic explanation! This really helped clear up my confusion. Thank you for sharing such valuable information.
https://mymilestonecard.cc/

답글 달기
comment-user-thumbnail
2025년 2월 21일

"Great explanation on handling multiple refresh token requests on the client side with JWT! Avoiding duplicate requests is crucial for performance and security. Also, that Tyler Durden Fight Club Costume Jacket reference was unexpected but awesome—just like the movie!"
Click Here: https://moviesleatherjacket.com/product/fight-club-tyler-durden-jacket

답글 달기

관련 채용 정보