안녕하세요! 🤪 이번 포스트에서는 클라이언트 사이드에서 다중 Refresh Token 요청을 처리하는 방법에 대해 이야기하고자 합니다.
JWT 로 인증과 인가를 구현하게 되면 RefreshToekn
은 필수 적으로 해야한다고 생각 합니다.
AccessToken 만 사용하게 되면 토큰탈취를 서버에서 알 수 없기 때문이죠.
Refresh Token에 대한 자세한 내용은 다음 기회에 작성하겠습니다.
JWT 인증과 인가, Promise 에 대한 사전 지식이 필요합니다!
액세스 토큰이 만료되었을 때, 리프레쉬 토큰으로 억세스 토큰을 재발급 받기 위해 ,
재발급 요청 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 가 필요 합니다.
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 이기 때문에
정말 유용하게 사용되는 클래스입니다.
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 의 비동기 관리는 항상 너무 재밌습니다. 🤣
긴 글 읽어 주셔서 감사합니다.
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.
Thanks for sharing such a useful and helpful blog. Keep doing your best.
https://mykohlscards.online/my-kohls-credit-card-payment/
I found the most beautiful and fascinating one. https://www.jacketsexpert.com/product/givenchy-letterman-jacket/
Excellent blog! Such clever work and exposure! Keep up the very good work. https://www.jacketscreator.com/product/top-boy-jamie-parachute-jacket/
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/
Great information about wilderness for beginners giving the opportunity for new people. https://www.jacketmakers.com/product/viva-la-vida-chris-martin-jacket/
Fantastic explanation! This really helped clear up my confusion. Thank you for sharing such valuable information.
https://mymilestonecard.cc/
"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
Very nice post. Keep sharing this kind of valuable post. A really great stuff posted here.
MCDVOICE