안녕하세요! 🤪 이번 포스트에서는 클라이언트 사이드에서 다중 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/
Very nice post. Keep sharing this kind of valuable post. A really great stuff posted here.
MCDVOICE