Axios Interceptor를 통해 Refresh Token으로 Access Token 재발급하기

이수빈·2023년 6월 4일
3

펫모리플젝

목록 보기
4/9
post-thumbnail
  • 프로젝트에서 Access Token이 만료되었을 때 Refresh Token을 통해 재발급 했던 코드를 정리하고자 한다.

  • 그전에 기반이 되는 HTTP프로토콜 특징과, 로그인 방식들, Interceptor에 대해 알아보자.

HTTP 프로토콜의 비연결성, 무상태성

비연결성

  • 비연결성(ConnectionLess)이란,클라이언트와 서버가 한 번 연결을 맺은 후, 클 라이언트 요청에 대해 서버가 응답을 마치면 맺었던 연결을 끊어 버리는 성질이다.

  • HTTP의 버전에 따라 다르지만, HTTP 1.0은 하나에 요청에 대한 응답마다 TCP의 신뢰성을 수립하고 끊는 과정(3-way handshake, 4-way handshake)이 발생한다.

  • 1.1 버전 부터는 Keep-Alive 기능이 추가되어서 이러한 비연결성이 어느정도 해소되었다. 이는 3-way handshake를 통해 연결된 세션을 계속 없애지않고 연결을 특정시간동안 유지한다.

무상태성

  • 이러한 비연결성때문에 무상태성이라는 특징이 나타난다. 무상태성은 HTTP요청시 서버는 클라이언트의 이전 상태를 기억하지 못하는 성질이다.

  • 그래서 매번 요청을 할때마다 유저의 인증관련 정보를 보내주는 역할로 쿠키, 세션, 토큰등을 사용한다.

로그인방식

  • 로그인 방식은 아래 reference에 자세하게 정리된 글이 있다. 그 글을 참조하는 것을 추천한다.(필자도 저거보고 공부함)

세션+쿠키방식

  • 쿠키란? 클라이언트에서 서버에게 요청시 헤더에 담겨 전송되는 작은 데이터 조각이다.

  • 서버에 로그인 요청을 보내게 되면, 응답값으로 세션id를 쿠키에 넣어 보내준다.

  • 이후 요청시, 클라이언트에서 세션id가 들어있는 쿠키를 함께 보내주고, 받은 세션id값을 바탕으로 서버에서 세션을 조회해 유저정보를 찾는다.

  • 보통 세션 저장소로는 Redis를 많이 사용한다고 한다.

  • 결국 인증의 책임을 서버가 지게하기 위해 세션방식을 사용한다.

  • 세션 쿠키방식은 몇가지 단점이 존재하는데, 먼저 서버측에서 세션을 위한 저장소를 추가로 사용해 추가적인 저장공간과 부하가 높아지게 되고, 해커가 http요청에서 탈취한 쿠키를 통해 재요청을 보낸다면, 사용자 정보를 악의적으로 이용 할 수 있는 가능성이 존재한다.

토큰 기반 인증방식(jwt) + Refresh Token

  • 이를 보완한 것이 토큰기반 인증방식이다.

  • 사용자가 로그인을 하면, 서버측에서는 Secret Key를 이용해 암호화된 Access Token을 발급해준다.

  • 사용자는 이후 인증이 필요한 요청마다 헤더에 토큰을 함께 실어 보내는 방식이다.

  • 서버에서는 이 토큰을 가지고 Secret Key를 이용해 토큰을 복호화 한 후 유효기간을 확인하고 사용자에 맞는 데이터를 가져온다.

  • 즉, 토큰안에 유저정보가 들어있기 때문에 세션과 같이 별도의 저장소를 사용 할 필요가 없는 장점이 있다. 또한 서버입장에서 확정성이 뛰어나다.(소셜로그인은 토큰기반으로 구현됨)

  • 하지만, 이미 탈취당한 토큰은 해커가 악의적으로 사용가능하기 때문에, 보안을 높히려고 access token의 유효기간을 짧게 설정하고 만약 만료시 refresh Token을 통해 재발급하는 방법을 많이 사용한다.

Axios Interceptor

  • Interceptor를 사용하면 request와 response를 가로채 공통적인 로직을 처리 할 수 있게 해준다.

  • 프로젝트에서는 instance를 만들어 api 요청을 처리했는데, instance마다 401에러나, 토큰재발급을 위한 공통 로직을 처리하기 위해 interceptor를 활용하였다.

  • request를 위한 interceptor와 response를 위한 interceptor가 존재하는데, 공식문서의 코드를 활용 코드를 보면 다음과 같다.

// 요청 인터셉터 추가하기
axios.interceptors.request.use(function (config) {
    // 요청이 전달되기 전에 작업 수행
    return config;
  }, function (error) {
    // 요청 오류가 있는 작업 수행
    return Promise.reject(error);
  });

// 응답 인터셉터 추가하기
axios.interceptors.response.use(function (response) {
    // 2xx 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
    // 응답 데이터가 있는 작업 수행
    return response;
  }, function (error) {
    // 2xx 외의 범위에 있는 상태 코드는 이 함수를 트리거 합니다.
    // 응답 오류가 있는 작업 수행
    return Promise.reject(error);
  });
  • 만약 accessToken이 존재하지 않는다면, 로그인 페이지로 redirect할 수 있도록 request Interceptor에서 처리해주었다.

  • 토큰이 존재한다면, 토큰을 넣어 요청을 보낸다.

// API.ts

const instance = axios.create();

instance.defaults.withCredentials = true;
instance.defaults.baseURL = 'http://52.78.181.46';

instance.interceptors.request.use(
  (config) => {
    const accessToken = getAccessToken();

    if (!accessToken) {
      window.location.href = '/login';
      return config;
    }

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

    return config;
  },
  (error: any) => {
    console.log(error);
    return Promise.reject(error);
  },
);
  • response Interceptor에서는 응답을 받은 뒤 에러코드에 따라 에러를 처리했다.

  • 에러의 상태가 404이면 notFound Page로 redirect 시켰고,

  • 401이면 권한없음 페이지로,

  • 만약 상태가 500이고, code가 7001이라면 토큰이 만료된 상태이다.

  • 토큰만료를 클라이언트에서 판단할 수도 있고, 서버에서 판단할 수 있지만, 서버에서 판단해 요청시 클라이언트에게 만료되었다는 errorCode를 보내주는 식으로 구현하였다.

//API.ts

instance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    if (error.response?.status === 401) {
      window.location.href = '/unauthorized';
    } 
    else if (error.response?.status === 404) {
      window.location.href = '/notFound';
    } 
    else if (error.response && error.response?.status === 500) {
      const errorCode = error.response.data.errorCode;
      if (errorCode === 7001) {
        await tokenRefresh(instance);
        const accessToken = getAccessToken();
        error.config.headers.Authorization = `Bearer ${accessToken}`;
        // 중단된 요청을(에러난 요청)을 토큰 갱신 후 재요청
        return instance(error.config);
      }
    }
  • 토큰이 만료되었다면, Refresh Token을 통해 Access Token을 재발급 한 후, tokenRefresh함수를 통해 재발급된 토큰을 통해 재요청을 보낸다.

  • 토큰 만료를 확인하는 함수도 구현했지만, 서버에서 이미 만료를 판단해 에러코드를 보내주기때문에 사용하지 않았다.

//ApiUtil.ts

export const tokenRefresh = async (instance: AxiosInstance) => {
  const refreshToken = getRefreshToken(); // 리프레시 토큰을 가져오기

  const { data } = await instance.get('/reissue', {
    headers: { 'Content-Type': 'application/json', RefreshToken: `Bearer ${refreshToken}` },
  });

  const newAccessToken = data.accessToken;
  sessionStorage.setItem('Authorization', newAccessToken); // 세션 스토리지에 액세스 토큰 저장
}; // tokenRefresh() - 토큰을 갱신해주는 함수

export const isTokenExpired = () => {
  const accessToken = getAccessToken();
  if (!accessToken) {
    return true;
  }
  const decodedToken = jwtDecode<JwtPayload>(accessToken);
  const currentTime = Date.now() / 1000;
  if (decodedToken.exp !== undefined && decodedToken.exp < currentTime) {
    // 토큰이 만료된 경우
    return true;
  }
  return false;
}; // isTokenExpired() - 토큰 만료 여부를 확인하는 함수

ref)
http : https://junhyunny.github.io/information/http-keep-alive/
세션,쿠키방식 : https://tansfil.tistory.com/58?category=475681
Access + RefreshToken : https://tansfil.tistory.com/59?category=475681
axios Intercetpor 공식문서 : https://axios-http.com/kr/docs/interceptors

profile
응애 나 애기 개발자

0개의 댓글