[axios] 토큰 만료시 재요청

chosh·2023년 6월 29일

토큰 만료시 재요청

백엔드 개발자와 회의 하면서, 토큰 전략을 고민했는데 아래와 같이 구현하였다.

accessToken 만료시간: 2시간

refreshToken 만료시간: 2개월

으로 토큰 만료시간 책정


accessToken 만료시 refreahToken 으로 새로운 accessToken을 발급 받는다

이때, refreshToken도 함께 다시 발급받는다

왜 refreshToken 만료기간은 2개월인데 함께 발급 받기로 했는지…

refreshToken의 역할은 2개월동안 새로 접속 했을때 다시 로그인 없이 서비스를 이용할 수 있도록 하는거라 생각했다. 이 이유만으로는 다시 발급 받는 이유가 되지는 않는데, 다시 발급 받는 이유는 accessToken의 만료시간을 2시간으로 잡은 이유와 동일하다고 생각한다.

accessToken의 만료시간을 2시간으로 잡은 이유는 토큰이 탈취 당했다고 하더라도 2시간 후면 accessToken의 효력을 잃어 해커의 접근을 차단 할 수 있다.
그런데 refreshToken을 탈취당하고, 만료시간 2개월이 지나고 새로 발급받는다고 한다면, 해커는 2달동안 accessToken을 새로 발급 받아 마음껏 서비스를 이용할 수 있게 된다.

그렇게 되면 원래 주인인 유저가 접근할때도 가지고 있는 refreshToken으로 새로운 accessToken을 발급 받을 것이고, 해커가 접근할때도 새로운 accessToken을 발급 받게 되는데, 이러면 실제로 계정에 있는 자산을 탈취당하지 않는 이상, 해커가 본인의 계정을 마음껏 이용하고 있는지도 모르게 된다.

그래서 accessToken을 새로 발급받을때마다 refreshToken을 새로 발급받는다.
이렇게 된다면, 해커가 refreshToken을 탈취한다고 해도, 두시간이 지나면 새로운 token을 가지게 되고, 유저의 자동로그인이 풀리거나, 해커가 가지고 있는 token이 무용지물이 된다.

프론트엔드 파트에서 할 일

  1. 로그인시 발급받는 accessToken, refreshToken 저장
  2. accessToken으로 계정으로 접근해야 되는부분의 api마다 요청
  3. 토큰 만료 에러(403(권한에러)) 발생시 refreshToken으로 업데이트 요청
  4. 새로 발급받은 accessToken, refreshToken 저장
  5. 만료로 실패한 api 요청 재전송

코드 설명

인스턴스 생성

const instance = axios.create();
const _instance = axios.create();

두개를 생성해주는 이유

  • 토큰 만료에러를 interceptor로 감지를 하고 token 업데이트 요청을 보낼때 사용하기 위함
  • fetch로 업데이트 요청을 보내도 됨
  • 하나의 instance로 했을 경우
    • 보통 권한에러로 403을 보내게 되는데, token이 둘다 만료되었을때 interceptor의 interceptor가 작동하고, 그 interceptor의 interceptor가 작동하고… * 100…

interceptor (요청 가로채기)

instance.interceptors.response.use(
  ...
);

403이라는 응답을 요청한곳에서 받기 전에 가로채는 것이기 때문에 response를 가로챈다

재요청 코드 작성

// 정상적인 리스폰스면 그 리스폰스 반환
(response) => {
  return response;
},

// 에러 발생 시
async (error) => {
  const {
    config, // 기존에 요청했던 config가 전달됨
    response: { status, message }, // 서버에서 준 에러 메세지
  } = error;

  if (status === 403) { // 토큰 만료면 토큰 업데이트 요청
    const originalRequset = config; // 기존 요청
    const refreshToken = sessionStorage.getItem("REFRESH_TOKEN");

    const headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${refreshToken}`,
    }; // refresh toekn 으로 요청하기 위해 header를 만듬

    try {
      const { data } = await _instance({
        method: "GET",
        url: process.env.REACT_APP_BASEURL + "/token/update",
        headers: headers,
      }); // interceptor 가 없는 인스턴스로 토큰 업데이트 요청

			// 요청 성공시 새로운 토큰 반환
      const newAccessToken = data.data.accessToken;
      const newRefreshToken = data.data.refreshToken;

			// 기존 실패한 요청에서 토큰만 새로운 토큰으로 교체
      originalRequset.headers["Authorization"] = `Bearer ${newAccessToken}`;

			// 새로 발급 받은 토큰 저장
      sessionStorage.setItem("ACCESS_TOKEN", newAccessToken);
      sessionStorage.setItem("REFRESH_TOKEN", newRefreshToken);

			// 재 요청 갑니다
      const res = await _instance(originalRequset);
      return res;
    } catch (err: any) {
			// 만료되서 에러 발생시 로그인 페이지로 이동 시키기
      const { status, message } = err.response.data;
      if (status === 403) {
        alert(message);
        window.location.href = "/";
      }
    }
  }

  return Promise.reject(error);
}

최종 코드

const instance = axios.create();
const _instance = axios.create();

instance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const {
      config,
      response: { status, message },
    } = error;

    if (status === 403) {
      const originalRequset = config;
      const refreshToken = sessionStorage.getItem("REFRESH_TOKEN");

      const headers = {
        "Content-Type": "application/json",
        Authorization: `Bearer ${refreshToken}`,
      };

      try {
        const { data } = await _instance({
          method: "GET",
          url: process.env.REACT_APP_BASEURL + "/token/update",
          headers: headers,
        });

        const newAccessToken = data.data.accessToken;
        const newRefreshToken = data.data.refreshToken;

        originalRequset.headers["Authorization"] = `Bearer ${newAccessToken}`;

        sessionStorage.setItem("ACCESS_TOKEN", newAccessToken);
        sessionStorage.setItem("REFRESH_TOKEN", newRefreshToken);

        const res = await _instance(originalRequset);
        return res;
      } catch (err: any) {
        const { status, message } = err.response.data;
        if (status === 403) {
          alert(message);
          window.location.href = "/";
        }
      }
    }

    return Promise.reject(error);
  }
);
profile
제가 참고하기 위해 만든 블로그라 글을 편하게 작성했습니다. 틀린거 있다면 댓글 부탁드립니다.

0개의 댓글