[React] axios interceptor

jinjoo-jung·2023년 12월 9일
0

React

목록 보기
7/10
  • 로그인 기능을 구현할 때 주로 토큰 기반 인증으로 구현하는데, 이때 JWT(Json Web Token)를 많이 사용
  • 토큰 인증 방식은 좋은 방법이긴 하지만, 제 3자에게 토큰이 노출되었을 경우 보안에 취약하다는 단점

=> 토큰 인증을 구현할 때 유효기간을 짧게 설정한 accessToken으로 인증하고, 이 accessToken으로이 만료되었을 때,유효기간을 길게 설정한 refreshToken을 통해 accessToken을 재발급 받는다.

axios interceptor

토큰 인증을 받기 위해서 클라이언트에 저장된 토큰을 서버로 보내야하며

  • 이 과정이 로그인 시 이루어져야 한다.
    => 로그인 할 때 이루어지려면 비동기 통신 시 토큰 정보를 보내야하는데, 이때 axios interceptor를 사용

    axios interceptor란, 비동기 통신이 이루어지기 정에 요청이나 응답을 가로채 작업을 추가로 수행한 뒤 통신이 이루어지게 하는 기능
    별도의 Library가 아닌 axios에 포함된 기능이다. 요청이나 응답 전에 무엇인가를 수행해주거나, 오류 발생시에 수행할 것들을 미리 정의해둘 수 있다.

    토큰 인증시 요청을 보낼 때 헤더에 accessToken을 함께 실어보내고, 응답을 받을 때 에러가 발생(accessToken 만료)하면 refreshToken을 확인하여 재발급을 요청한다.

과정

  • 요청성공시
    return

  • 요청실패시 ( 토큰 만료로 인한 에러 발생시)

  1. 에러를 반환하기 전에 interceptor 해서, 서버에 token refresh를 요청합니다.
  2. token refresh요청을 통해 받은 새로운 accessToken을 ReduxStore등 본인이 이용하는 저장소에 저장합니다.
  3. 새로운 accessToken을 header에 담아, 재요청합니다.

코드 예시

import axios from "axios";

// url 호출 시 기본 값 셋팅
const api = axios.create({
  baseURL: "https://api.themoviedb.org/3",
  headers: { "Content-type": "application/json" }, // data type
});

// Add a request interceptor
api.interceptors.request.use(
  function (config) {
    const token = localStorage.getItem("token");

    //요청시 AccessToken 계속 보내주기
    if (!token) {
      config.headers.accessToken = null;
      config.headers.refreshToken = null;
      return config;
    }

    if (config.headers && token) {
      const { accessToken, refreshToken } = JSON.parse(token);
      config.headers.authorization = `Bearer ${accessToken}`;
      config.headers.refreshToken = `Bearer ${refreshToken}`;
      return config;
    }
    // Do something before request is sent
    console.log("request start", config);
  },
  function (error) {
    // Do something with request error
    console.log("request error", error);
    return Promise.reject(error);
  }
);

// Add a response interceptor
api.interceptors.response.use(
  function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    console.log("get response", response);
    return response;
  },
  async (error) => {
    const {
      config,
      response: { status },
    } = error;
    if (status === 401) {
      if (error.response.data.message === "expired") {
        const originalRequest = config;
        const refreshToken = await localStorage.getItem("refreshToken");
        // token refresh 요청
        const { data } = await axios.post(
          `http://localhost:3000/refreshToken`, // token refresh api
          {},
          { headers: { authorization: `Bearer ${refreshToken}` } }
        );
        // 새로운 토큰 저장
        // dispatch(userSlice.actions.setAccessToken(data.data.accessToken)); store에 저장
        const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
          data;
        await localStorage.multiSet([
          ["accessToken", newAccessToken],
          ["refreshToken", newRefreshToken],
        ]);
        originalRequest.headers.authorization = `Bearer ${newAccessToken}`;
        // 401로 요청 실패했던 요청 새로운 accessToken으로 재요청
        return axios(originalRequest);
      }
    }
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    console.log("response error", error);
    return Promise.reject(error);
  }
);

export default api;
  1. 로그인 및 회원가입에 성공하면 엑세스토큰, 리프레쉬토큰을 서버로부터 받는다
  2. 토큰(인증)이 필요한 api 요청시 헤더에 key:Authorization, value:accessToken을 담아서 보낸다.
  3. 그러다 서버로부터 401 코드와 함께 에러메시지를 받게 된다면, 헤더에 리프레쉬 토큰을 담아서 다시 api 요청.
  4. 리프레쉬 토큰을 받은 서버는 해당 토큰이 유효한지 판단한 후 새로운 엑세스토큰을 응답에 담아서 보낸다.
  5. 클라이언트는 새로 받은 엑세스토큰을 헤더에 담아서 다시 api 요청. (엑세스토큰만 담는다?)
  • 리프레쉬 토큰이 만료된 경우, 재로그인?
  • 서버로부터 받은 token이 Bearer를 모두 붙여서 보낸다.?

=> 요약 : 엑세스토큰보다 유효기간이 긴 리프레쉬 토큰을 추가하여 엑세스토큰을 재발급한다. 리프레쉬 토큰이 만료된느 경우에는 다시 로그인을 해야한다.

Axios 인터셉터를 적용하려는 이유❓

토이 프로젝트를 진행하면서 서버에 토큰 인증을 필요로 하는 API 요청을 할때마다 HTTP Authorization 요청 헤더에 토큰을 넣어줘야하고 401(Unauthorized) 에러가 서버로부터 들어오면 토큰을 갱신해준 후 재요청을 보내는 과정을 한 곳에서 모두 처리하여 중복 코드를 제거하고 유지보수성을 향상시키기 위해 Axios 인터셉터를 적용하기로 했다.
사용자 지정 config로 새로운 Axios 인스턴스를 생성하여 사용하니 더욱 편리했다. (baseURL, timeout 설정)

1. axios 인스턴스 생성

const instance = axios.create({
  // 상대적인 URL을 인스턴스 메서드에 전달하려면 baseURL을 설정하는 것은 편리하다.
  // URL(서버 주소) 예시 - http://127.0.0.1:5500
  baseURL: URL,
  // 요청이 timeout보다 오래 걸리면 요청이 중단된다.
  timeout: 1000,
});

2. 요청 인터셉터 추가

  • 요청이 전달되기 전에 작업 수행, 요청 오류가 있는 작업 수행을 할 수 있는 2개의 콜백 함수를 받는다.
instance.interceptors.request.use(
  (config) => {
    // getToken() - 클라이언트에 저장되어 있는 액세스 토큰을 가져오는 함수
    const accessToken = getToken();

    config.headers['Content-Type'] = 'application/json';
    config.headers['Authorization'] = `Bearer ${accessToken}`;

    return config;
  },
  (error) => {
    console.log(error);
    return Promise.reject(error);
  }
);

3. 응답 인터셉터 추가하기

  • 응답 데이터가 있는 작업 수행, 응답 오류가 있는 작업 수행을 할 수 있는 2개의 콜백함수받기
instance.interceptors.response.use(
  (response) => {
    if (response.status === 404) {
      console.log('404 페이지로 넘어가야 함!');
    }

    return response;
  },
  async (error) => {
    if (error.response?.status === 401) {
      // isTokenExpired() - 토큰 만료 여부를 확인하는 함수
      // tokenRefresh() - 토큰을 갱신해주는 함수
      if (isTokenExpired()) await tokenRefresh();

      const accessToken = getToken();

      error.config.headers = {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${accessToken}`,
      };

      // 중단된 요청을(에러난 요청)을 토큰 갱신 후 재요청
      const response = await axios.request(error.config);
      return response;
    }
    return Promise.reject(error);
  }
  );

https://gusrb3164.github.io/web/2022/08/07/refresh-with-axios-for-client/
참고 출처

https://velog.io/@bnb8419/Axios-Interceptor를-통해-Refresh-Token으로-Access-Token-재발급하기

profile
개인 개발 공부, 정리용 🔗

0개의 댓글

관련 채용 정보