axios interceptor로 인증 필요한 api 개선해보기

정승연·2024년 11월 27일
0
post-thumbnail

현재 우리의 서비스는 로그인을 하지 않고 이용이 가능한 api와 로그인을 한 뒤 access_token을 통해 접근이 가능한 api 들이 존재한다.
로그인을 통해 접근이 가능한 api는 access_token이 만료될 시 401 에러가 발생하며 refresh_token을 통해 access_token을 다시 재발급 받아야 재인증이 가능하다.
위의 access_token을 재발급 받는 로직을 구현하기 위해 axios interceptor로 구현을 해보았다

axios interceptor 사용 전

export const instance = axios.create({
	baseURL: import.meta.env.VITE_API_BASE_URL,
	timeout: 2000,
});

export async function myAccount(token:string): Promise<Account> {
 try {
   const response = await instance .get(`/account/myaccount`,{
	   headers:{
			   Authorization:`Bearer ${token}`
			 }
   });
   return response.data;
 } catch (error) {
   // axios 에러인지 체크
   if (axios.isAxiosError(error)) {
     if (error.response?.status === 401) {
       try {
         // refresh token으로 새 access token 발급 요청
         const refreshResponse = await instance.post<{access_token: string}>(
           '/auth/refresh',
           {},
           { withCredentials: true }
         );

         // 새 토큰 저장
         const new_access_token = refreshResponse.data.access_token;
         localStorage.setItem('access_token', new_access_token);

         // 원래 요청 재시도
         const retryResponse = await authInstance.get('/account/myaccount', {
           headers: {
             Authorization: `Bearer ${new_access_token}`
           }
         });
         
         return retryResponse.data;
       } catch (refreshError) {
         // refresh token도 만료된 경우
         localStorage.removeItem('access_token');
         window.location.href = '/';  // 로그인 페이지로 리다이렉트
         throw new Error('세션이 만료되었습니다. 다시 로그인해주세요.');
       }
     }
   }
   throw error; // 401 이외의 에러는 그대로 throw
 }
}
  • axios interceptor를 사용하기 전 우리의 코드는 하나의 api 인스턴스를 사용하고 있었다.
  • axios 에러 상태 코드를 확인하여 401 에러가 발생하면 토큰의 재요청 로직을 보내는 어렵지 않은 로직임을 확인할 수 있다.
  • 하지만 이런 방식은 인증이 필요한 각 API 함수마다 토큰 갱신 로직을 중복 작성해야 한다는 단점이 존재한다
  • 실제로 우리의 서비스에서 인증이 필요한 api 들은 myAccount 외 엄청나게 많이 존재하기에 위의 문제를 개선해야했다.

axios interceptor

axios 인터셉터는 http 요청이나 응답을 보내기 전/후에 가로채서 처리할 수 있게 해주는 기능이다

Request 인터셉터

axios.interceptors.request.use(
  // 요청 보내기 전 실행되는 함수
  (config) => {
    // 요청 성공 처리
    return config;
  },
  // 요청 에러 시 실행되는 함수
  (error) => {
    // 요청 에러 처리
    return Promise.reject(error);
  }
);
  • request 인터셉터는 api 요청을 보내기 전 api의 설정을 추가할 수 있다.

Response 인터셉터

// 2. Response 인터셉터
axios.interceptors.response.use(
  // 응답을 받은 후 실행되는 함수
  (response) => {
    // 응답 성공 처리
    return response;
  },
  // 응답 에러 시 실행되는 함수
  (error) => {
    // 응답 에러 처리
    return Promise.reject(error);
  }
);
  • response 인터셉터는 api 요청을 보낸 후 응답을 받은 뒤 로직을 추가할 수 있다.

인증 로직 처리 Request 인터셉터

authInstance.interceptors.request.use(
	(config) => {
		const access_token = localStorage.getItem('access_token');
		if (access_token) {
			config.headers.Authorization = `Bearer ${access_token}`;
		}
		return config;
	},
	(error) => {
		return Promise.reject(error);
	},
);
  • 기본적으로 우리의 인증 로직은 헤더에 access_token 을 담아서 보내주기에 위와 같이 request 인터셉터에는 로컬 스토리지에 보관하고 있는 access_token을 가져와 헤더에 담아준다
  • 위 로직을 request 인터셉터로 처리하면 header 설정을 해주는 코드의 중복을 줄일 수 있다

인증 로직 처리 Response 인터셉터

export const authInstance = axios.create({
	baseURL: import.meta.env.VITE_API_BASE_URL,
	timeout: 2000,
	withCredentials: true,
});

authInstance.interceptors.response.use(
	(response) => response,
	async (error) => {
		const originalRequest = error.config;
		if (error.response.status === 401 && !originalRequest._retry) {
			try {
				const response: AxiosResponse<Login> = await instance.post(
					'/auth/refresh',
					{
						refreshToken: getCookie('refresh_token'),
					},
					{ withCredentials: true },
				);
				const new_access_token = response.data.access_token;
				const new_refresh_token = response.data.refresh_token;
				localStorage.setItem('access_token', new_access_token);
				setCookie('refresh_token', new_refresh_token, {
					path: '/',
					httpOnly: true,
				});
				originalRequest.headers.Authorization = `Bearer ${new_access_token}`;
				return authInstance(originalRequest);
			} catch (error) {
				localStorage.removeItem('access_token');
				removeCookie('refresh_token');
				window.location.href = '/';
				return Promise.reject(error);
			}
		}
		return Promise.reject(error);
	},
);
  • response 인터셉터에서 401(인증만료) 에러가 발생했을 때 토큰 갱신 로직이다.
  • 401 에러가 발생하면 기존의 refres_token을 활용해 새로운 토큰을 받아온다.
  • 만약 access_token을 재발급 받는 로직에 에러가 발생한다면 refresh_token 이 만료됐다는 뜻으로 쿠키를 제거한다

개선 결과

export async function myAccount(): Promise<Account> {
	const response = await authInstance.get(`/account/myaccount`);
	return response.data;
}
  • authInstance 만 사용하면 로그인 인증에 필요한 로직을 interceptor 가 알아서 처리해주기에 위와 같이 코드가 간단해진 것을 볼 수 있다.
profile
지속가능한 개발자

0개의 댓글