axios interceptors를 활용한 토큰 첨부 & 토큰 재발급

hzn·2023년 5월 10일
1

PROJECT🏝

목록 보기
23/24
post-thumbnail

axios interceptors

  • axios interceptors를 사용하면 then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있다.
  • request에서는 요청을 보내기 전에 하는 작업이 가능하고
  • response에서는 서버에서 받은 응답이 return 되기 전(= then과 catch로 넘어가기 전)에 인터셉터로 가로채서 원하는 작업들을 추가할 수 있다.
  • 커스텀 인스턴스를 만들어 interceptors를 사용할 수도 있다.

기본 예시

요청(request) interceptors

axios.interceptors.request.use(function (config) {
    // 요청이 전달되기 전, 요청(config)에 대한 설정 작업
    return config;
  });

응답(response) interceptors

axios.interceptors.response.use(
 // 정상 응답 처리 (이 작업 이후 .then()으로 이어진다)
  (response) => {
    return response;
  },
  // 에러 처리 (이 작업 이후 .catch()로 이어진다)
  async (error) => {
  // 응답이 error일 때 처리할 작업 
      return Promise.reject(error);
}
);

프로젝트 적용

0. axios interceptors 사용 설정

import axios, { AxiosRequestConfig } from 'axios';

const REQUEST_URL = 'https://요청 API';

const config: AxiosRequestConfig = { baseURL: REQUEST_URL };
const axiosInstance = axios.create(config);
  • 요청 config에서 baseURL을 설정해놓으면 편하다.
  • 인스턴스(axiosInstance)를 생성해서 interceptors 적용할 요청만 axiosInstance를 사용하려고 한다.

1. [요청 전] 토큰이 필요한 모든 요청의 헤더에 토큰 넣어서 보내기

// [요청 설정] 모든 요청의 헤더에 토큰 넣어 보내기
axiosInstance.interceptors.request.use((config) => {
  // if (!config.headers) return config;

  const access_token = Cookies.get('access_token');
  const refresh_token = Cookies.get('refresh_token');

  if (config.url === '/api/reissue') {
    // 토큰 재발급 요청일 때만  헤더에 refresh_token 넣어서 보내고
    config.headers.Refresh = refresh_token;
  } else {
    // 그 외 요청은 헤더에 access_token 넣어서 보내기
    config.headers.Authorization = access_token;
  }

  return config;
});

2. [응답 전] access_token 만료 시 토큰 재발급

access_token이 만료돼서 402 error로 응답이 올 경우, refresh_token을 헤더에 담아 토큰 재발급 요청을 보낸 후 재발급받은 access_token을 헤더에 담아 기존 요청을 다시 보낸다.

1) 토큰 재발급 요청 함수 만들기

  • 새로 발급받은 access_token을 쿠키에 저장하고 반환
// access_token 재발급 요청 (access_token 반환))
const reIssuedToken = async () => {
  try {
    await axiosInstance.get(`/api/reissue`).then((res) => {
      Cookies.set('access_token', res.headers.authorization, {
        expires: 0.079,
      });

      if (res.headers.refresh) {
        Cookies.set('refresh_token', res.headers.refresh, {
          expires: 20,
        });
      }
    });

    return Cookies.get('access_token'); // 재발급받은 access_token 반환
  } catch (e) {
    Cookies.remove('access_token', { path: '' });
    Cookies.remove('refresh_token', { path: '' });
    Cookies.remove('nickName', { path: '' });
    router.push('/login');
  }
};

2) 402 에러 시 토큰 재발급

  • 정상 응답의 경우 그대로 response 반환
  • 402 에러의 경우 refresh_token을 헤더에 담아 토큰 재발급 요청을 보낸 후 재발급받은 access_token을 헤더에 담아 기존 요청을 다시 보낸다.
// [응답 설정]
axiosInstance.interceptors.response.use(
  // 정상 응답 처리
  (response) => {
    return response;
  },
  // 에러 처리
  async (error) => {
    const { config, response } = error;

    // 토큰 자동 재발급 필요 외 다른 에러
    if (
      config.url === `/api/reissue` ||
      response?.status !== 402 ||
      config.sent
    ) {
      return Promise.reject(error);
    }

    config.sent = true;
    const access_token = await reIssuedToken(); // 토큰 재발급 받아서

    if (access_token) {
      config.headers.Authorization = access_token; // 헤더에 넣어서
    }

    return axiosInstance(config); // 다시 요청
  }
);

3. [응답 전] 공통 Error 처리

(적용 예정)

4. 사용하기

  • 인증(토큰)이 필요한 요청일 경우
    : 요청 헤더에 토큰 자동으로 넣어주고 402 에러 응답시 토큰 재발급 요청 설정이 되어있는 axiosInstance를 사용
// 일기 생성
export const createDiary = async (form: DiaryProps) =>
  axiosInstance.post('/api/diary', form);

// 날짜별 일기 데이터 가져오기 (Client Side)
export const getDiaryByDate = async (date: string) =>
  axiosInstance.get(`/api/diary/date?createdAt=${date}`);
  • 인증(토큰)이 필요 없는 요청일 경우
    : 그냥 axios 사용
// id별 일기 데이터 가져오기 (Server Side / token 필요 X)
export const getDiaryById = async (id: number | string) => {
  const res = await axios.get(`${REQUEST_URL}/api/diary/${id}`);

  return res.data;
};

전체 코드

api > diary.ts

import axios, { AxiosRequestConfig } from 'axios';
import Cookies from 'js-cookie';
import router from 'next/router';

const REQUEST_URL = 'https://sentiment-diary.store';

const config: AxiosRequestConfig = { baseURL: REQUEST_URL };
const axiosInstance = axios.create(config);

// [요청 설정] 모든 요청의 헤더에 토큰 넣어 보내기
axiosInstance.interceptors.request.use((config) => {
  // if (!config.headers) return config;

  const access_token = Cookies.get('access_token');
  const refresh_token = Cookies.get('refresh_token');

  if (config.url === '/api/reissue') {
    // 토큰 재발급 요청일 때만  헤더에 refresh_token 넣어서 보내고
    config.headers.Refresh = refresh_token;
  } else {
    // 그 외 요청은 헤더에 access_token 넣어서 보내기
    config.headers.Authorization = access_token;
  }

  return config;
});

// access_token 재발급 요청 (access_token 반환))
const reIssuedToken = async () => {
  try {
    await axiosInstance.get(`/api/reissue`).then((res) => {
      Cookies.set('access_token', res.headers.authorization, {
        expires: 0.079,
      });

      if (res.headers.refresh) {
        Cookies.set('refresh_token', res.headers.refresh, {
          expires: 20,
        });
      }
    });

    return Cookies.get('access_token');
  } catch (e) {
    Cookies.remove('access_token', { path: '' });
    Cookies.remove('refresh_token', { path: '' });
    Cookies.remove('nickName', { path: '' });
    router.push('/login');
  }
};

// [응답 설정]
axiosInstance.interceptors.response.use(
  // 정상 응답 처리
  (response) => {
    return response;
  },
  // 에러 처리
  async (error) => {
    const { config, response } = error;

    // 토큰 자동 재발급 필요 외 다른 에러
    if (
      config.url === `/api/reissue` ||
      response?.status !== 402 ||
      config.sent
    ) {
      return Promise.reject(error);
    }

    config.sent = true;
    const access_token = await reIssuedToken(); // 토큰 재발급 받아서

    if (access_token) {
      config.headers.Authorization = access_token; // 헤더에 넣어서
    }

    return axiosInstance(config); // 다시 요청
  }
);

type DiaryProps = {
  title: string;
  content: string;
  createdAt: string;
};

// 일기 생성
export const createDiary = async (form: DiaryProps) =>
  axiosInstance.post('/api/diary', form);

// 날짜별 일기 데이터 가져오기 (Client Side)
export const getDiaryByDate = async (date: string) =>
  axiosInstance.get(`/api/diary/date?createdAt=${date}`);

// 유저별 일기 데이터 가져오기
export const getDiaryByUser = async () =>
  axiosInstance.get(`/api/diary?page=1&size=10`);

// 수정하기
export const editDiary = (id: number, form: any) =>
  axiosInstance.patch(`/api/diary/${id}`, form);

// 삭제하기
export const deleteDiary = (id: number | undefined) =>
  axiosInstance.delete(`/api/diary/${id}`);

// id별 일기 데이터 가져오기 (Server Side / token 필요 X)
export const getDiaryById = async (id: number | string) => {
  const res = await axios.get(`${REQUEST_URL}/api/diary/${id}`);

  return res.data;
};

// [분석 페이지] 기간별 일기 데이터 가져오기
export const getDiaryByTerm = async (startDate: string, endDate: string) =>
  axiosInstance.get(
    `/api/diary/term?startDate=${startDate}&endDate=${endDate}`
  );

0개의 댓글