axios)만료된 accessToken을 RefreshToken을 통해 새 accessToken으로 받아오기

김명성·2022년 9월 28일
2
post-custom-banner

유저 정보를 accessToken을 통해 받아오고, accessToken이 만료되었다면
refreshToken을 통해서 새 accessToken을 받아오는 로직


axiosInstance.interceptors.response.use((res) => {
  return res;
},
  async(err) => {
    
    const { config, response:{status}} = err;
    
    if(status === 403 ||  status === 500) {      
        const originalRequest = config;
        if(!isTokenRefreshing){

          isTokenRefreshing = true;
          const token = getStoredToken()
          const {data} = await axiosInstance.put('/member/newAccess',{},
          {
            headers:getNewJWTToken(token)
          });
          console.log(data);
          
        isTokenRefreshing = false;
        setStoredToken(data);
        axiosInstance.defaults.headers.common["X-ACCESS-TOKEN"] = data.accessToken;
        
        
        
        const retryOriginalRequest = new Promise((resolve) => {
          
          addRefreshSubscriber((accessToken:string,refreshToken:string) => {
            originalRequest.headers["X-ACCESS-TOKEN"] = accessToken
            originalRequest.headers["X-REFRESH--TOKEN"] = refreshToken
            resolve(axios(originalRequest))
          })
        })
        onTokenRefreshed(data.accessToken)
        return retryOriginalRequest;
        }
        return Promise.reject(err);
    }
  })

하나씩 뜯어보자면

axiosInstance.interceptors.response.use((res) => {
  return res;
}

interceptors는 response,request에 사용할 수 있다.
response는 응답을 받기 전에 처리하는 로직이고 request는 요청을 하기 이전에 처리한다.
여기서는 response로 처리.
interceptors.response.use는 2개의 callback을 받는다.
첫번째 callback은 정상적으로 response를 받았을 때(resolve) 어떻게 처리할 것인지, 두번째는 Error를 받았을 때 어떻게 처리할 것인지 로직을 작성한다.
정상적으로 응답을 받았을 때에는 응답 그 자체를 그대로 return한다.


async(err) => {
    
    const { config, response:{status}} = err;
    
    if(status === 403) {      
        const originalRequest = config;
        if(!isTokenRefreshing){

          isTokenRefreshing = true;
          const token = getStoredToken()

Error는 토큰 에러 뿐만 아니라 다양한 에러가 발생할 수 있기에 사전에
accessToken 에러라면 어떤 status를 받을 것인지 백앤드 개발자와 사전약속이 되어있어야 한다.

여기서는 403 status를 받기로 약속했다.

403에러를 받았다면, 새 accessToken을 넣어 준 뒤에 다시 요청을 보내줘야 하기에 config를 받아와야 한다.
여기서는 originalRequest라는 변수로 config를 할당했다.

isTokenRefreshing은 token을 사용하는 요청이 하나가 아니라 여러개가 동시 다발적으로 일어날 수 있기 때문에, 유효하지 않은 토큰으로 보내는 모든 응답들을 멈출 수 있게끔 해주는 flag의 기능을 한다.

getStoredToken()은 localStorage에 저장된 토큰을 가져오는 함수이다

export const getStoredToken = ():Token => {
  const storedUser = localStorage.getItem(LOCALSTORAGE_USER_KEY);
  
  return storedUser ? JSON.parse(storedUser) : null;
}

기존의 토큰을 가져온 뒤 새로운 토큰을 받아와야 한다.

	const {data} = await axiosInstance.put('/member/newAccess',{},
	{
		headers:getNewJWTToken(token)
	});
    
	isTokenRefreshing = false;
	setStoredToken(data);
	axiosInstance.defaults.headers.common["X-ACCESS-TOKEN"] = data.accessToken;

member/newAccess는 refreshToken을 headers에 담아 보내면 새로운 accessToken을 반환해주는 api이다.

토큰을 받아오고 setStoredToken을 통해 localStorage에 새로운 accessToken을 저장하고,
axios의 default X-ACCESS-TOKEN을 새로운 accessToken으로 교체해준다.

X-ACCESS-TOKEN
보통 Authorization: Bearer [accessToken] 형식으로 accessToken을 담아두지만 백앤드와 X-ACCESS-TOKEN으로 약속하였다.


새로운 토큰으로 다시 기존의 요청을 수행한다.

const retryOriginalRequest = new Promise((resolve) => {
          addRefreshSubscriber(
			(accessToken:string,refreshToken:string) => resolve(axios(originalRequest))
        })
        onTokenRefreshed(data.accessToken)
        return retryOriginalRequest;
        }
        return Promise.reject(err);
    }

addRefreshSubscriber는 아래와 같은 함수다

const addRefreshSubscriber = (callback:any) => {
  refreshSubscribers.push(callback)
}

콜백함수를 받아서 refreshSubscribers라는 배열에 받은 콜백 함수를 push해주는 역할을 한다.

refreshSubscribers 배열의 초깃값은 빈배열이다.

let refreshSubscribers:any[] = []; 

addRefreshSubscriber를 통해 콜백함수를 넣어주는데, 그 콜백함수는 403 에러가 발생된 request들이다.

(accessToken:string,refreshToken:string) => resolve(axios(originalRequest))
	

403 에러가 발생된 request는 onTokenRefreshed 함수를 통해 재전송 하는데 callback에는 여러개의 함수가 쌓일 수 있으므로 map을 통해 실행하고 인자로 새로 받은 accessToken을 넣어주면서 실행한다.

const onTokenRefreshed = (accessToken:any) => {
  refreshSubscribers.map((callback) => callback(accessToken))
}

새로운 요청 또한 실패했다면 Promise.reject(err)를 통해 error를 반환한다.

post-custom-banner

0개의 댓글