기록하는 개발자

[React] axios interceptor를 이용한 token refresh 본문

Web/React

[React] axios interceptor를 이용한 token refresh

밍맹030 2023. 4. 7. 15:10
728x90

현재 프로젝트에서는 access_token과 refresh_token을 받아 localstorage에 저장하여 사용자 인증 정보를 사용하고 있다.

두 토큰이 각각 만료되었을 때 처리 해야할 로직이 다르다.

 

access_token의 만료

프론트
access_token이 만료 된 채로 서버에 api 요청   
   서버에서 401 error 와 함께 message 로 “expired” 전송
기존의 access_token, refresh_token을 통해 토큰 재발급 요청   
  토큰 재발급
토큰 저장 후 토큰 만료로 수행하지 못했던 api 요청   
   response

 

refresh_token의 만료

프론트
refresh_token이 만료 된 채로 서버에 api 요청   
   ← 서버에서 401 error와 함께 message로 “만료된 refreshToken입니다.” 전송
Localstorage.clear(token 삭제)후 login page로 navigate하여 재로그인 유도  

 

 

TokenRefresher.tsx 전체 코드

import { useEffect } from "react";
import axios from "axios";
import { config } from "../static/config";
import { useNavigate } from "react-router-dom";

export default function TokenRefresher() {
  const navigate = useNavigate();

  useEffect(() => {
    const refreshAPI = axios.create({
      baseURL: `${config.api}`,
      headers: { "Content-type": "application/json" },
    });

    const interceptor = axios.interceptors.response.use(
      function (response) {
        return response;
      },
      async function (error) {
        const originalConfig = error.config;
        const msg = error.response.data.message;
        const status = error.response.status;
        
        if (status == 401) {
          if (msg == "access token expired") {
            await axios({
              url: `${config.api}/user/reissue`,
              method: "Post",
              headers: {
                accesstoken: localStorage.getItem("token"),
                refreshToken: localStorage.getItem("refreshToken"),
              },
            })
              .then((res) => {
                localStorage.setItem("token", res.data.accessToken);
                
                originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;

                return refreshAPI(originalConfig);
              })
              .then((res) => {
                window.location.reload();
              });
          } 
          else if (msg == "refresh token expired") {
            localStorage.clear();
            navigate("/login");
            window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
          } 
          else if (msg == "mail token expired") {
            window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
          }
        } 
        else if (status == 400 || status == 404 || status == 409) {
          window.alert(msg);
        }
        return Promise.reject(error);
      }
    );
    return () => {
      axios.interceptors.response.eject(interceptor);
    };
  }, []);
  return <></>;
}

 

axios instance 생성

const refreshAPI = axios.create({
	baseURL: `${config.api}`,
	headers: { "Content-type": "application/json" },
});

axios.create 를 통해 axios instance를 생성한다.

 

axios interceptor

const interceptor = axios.interceptors.response.use(
  function (response) {
    return response;
  },
  async function (error) {
    const originalConfig = error.config;
    const msg = error.response.data.message;
    const status = error.response.status;
        
    if (status == 401) {
      if (msg == "access token expired") {
        await axios({
          url: `${config.api}/user/reissue`,
          method: "Post",
          headers: {
            accesstoken: localStorage.getItem("token"),
            refreshToken: localStorage.getItem("refreshToken"),
          },
        })
        .then((res) => {
          localStorage.setItem("token", res.data.accessToken);
                
          originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;

          return refreshAPI(originalConfig);
        })
        .then((res) => {
          window.location.reload();
        });
      } 
      else if (msg == "refresh token expired") {
        localStorage.clear();
        navigate("/login");
        window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
      } 
      else if (msg == "mail token expired") {
        window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
      }
    } 
    else if (status == 400 || status == 404 || status == 409) {
      window.alert(msg);
    }
    return Promise.reject(error);
  }
);

 

interceptor 내부 상단의 함수

- 토큰 만료와 상관없이 api 요청에 성공하여 Reponse가 제대로 오는 경우이다.

const interceptor = axios.interceptors.response.use(
      //response가 정상적으로 오는 경우
      function (response) {
        return response;
      },

 

interceptor 내부 하단의 함수

- api 요청 후 error가 발생하는 경우이다.

  async function (error) {
    const originalConfig = error.config;
    const msg = error.response.data.message;
    const status = error.response.status;
        
    if (status == 401) {
      if (msg == "access token expired") {
        await axios({
          url: `${config.api}/user/reissue`,
          method: "Post",
          headers: {
            accesstoken: localStorage.getItem("token"),
            refreshToken: localStorage.getItem("refreshToken"),
          },
        })
        .then((res) => {
          localStorage.setItem("token", res.data.accessToken);
                
          originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;

          return refreshAPI(originalConfig);
        })
        .then((res) => {
          window.location.reload();
        });
      } 
      else if (msg == "refresh token expired") {
        localStorage.clear();
        navigate("/login");
        window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
      } 
      else if (msg == "mail token expired") {
        window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
      }
    } 
    else if (status == 400 || status == 404 || status == 409) {
      window.alert(msg);
    }
    return Promise.reject(error);
  }
);

 

변수 선언

const originalConfig = error.config;
const msg = error.response.data.message;
const status = error.response.status;

 

originalConfig : 기존에 수행하려 했던 작업

msg : 백에서 error response로 보내준 message

status : 현재 발생한 에러 코드

 

 

access_token 재발급

if (status == 401) {
  if (msg == "access token expired") {
    await axios({
          url: `${config.api}/user/reissue`,
          method: "Post",
          headers: {
            accesstoken: localStorage.getItem("token"),
            refreshToken: localStorage.getItem("refreshToken"),
          },
        })
        .then((res) => {
          localStorage.setItem("token", res.data.accessToken);
                
          originalConfig.headers["Authorization"]="Bearer "+res.data.accessToken;

          return refreshAPI(originalConfig);
        })
        .then((res) => {
          window.location.reload();
        });
      } 
  }
);

error의 status가 401이고 msg가 "access token expired" 이면 access_token이 만료됐다고 간주하여 토큰 재발급을 요청한다. 요청 응답이 정상적으로 오는 경우, localstorage의 access_token을 수정한다.

 

기존에 수행하려 했던 originalConfig의 header에는 이미 만료된 access_token이 들어있다. 따라서, 해당 header의 accessToken을 새로 받은 token으로 수정한다.

 

axios.create로 선언한 인스턴스를 통해 originalConfig를 요청한다. originalConfig가 정상적으로 수행되었다면 화면이 새로고침 된다.

 

 

refresh_token 재발급과 예외 처리

  else if (msg == "refresh token expired") {
    localStorage.clear();
    navigate("/login");
    window.alert("토큰이 만료되어 자동으로 로그아웃 되었습니다.");
  } 
  else if (msg == "mail token expired") {
    window.alert("비밀번호 변경 시간이 만료되었습니다. 다시 요청해주세요.");
  }
} 
else if (status == 400 || status == 404 || status == 409) {
  window.alert(msg);
}

error의 status가 401이고 msg가 "refresh token expired" 이면 refresh_token이 만료됐다고 간주한다.

이에 localStorage를 모두 비우고 login 화면으로 navigate하여 재로그인을 유도한다.

 

추가적으로 비밀번호 변경 시 이메일로 url에 token이 있는 비밀번호 변경 link를 보내주는데, 해당 token의 유효 시간은 10분이다. 10분이 지난 뒤 비밀번호 변경을 요청하면 401 에러가 발생하는데, 해당 에러의 핸들링을 위해 msg가 "mail token expired" 인 경우를 예외처리 하였다.

 

이외 400, 404, 409의 에러가 발생하는 경우, 서버에서 보내주는 msg를 경고창으로 띄워준다.

 

 

 

AppRouter.tsx 전체 코드

import 생략

const AppRouter = () => {
  return (
    <>
      <BrowserRouter>
        <TokenRefresher />
        <Navigation />
        <Routes>...</Routes>
      </BrowserRouter>
    </>
  );
};
export default AppRouter;

TokenRefresher 는 Navigation과 마찬가지로 Router 바깥에서 작동하도록 Routes 태그 바깥에 둔다.

728x90