JWT

[JWT] 로그인 세션 관리하기3 (토큰이 만료된다면? - Axios 인터셉터)

마뇨기 2024. 2. 2. 21:34

2024.02.01 - [javascript] - [JWT] 로그인 세션 관리하기 2 (Session Storage, Local Storage, Recoil)

 

[JWT] 로그인 세션 관리하기 2 (Session Storage, Local Storage, Recoil)

2024.02.01 - [javascript] - [JWT] 로그인 세션 관리하기 1 (JWT에 관하여) [JWT] 로그인 세션 관리하기1 (JWT에 관하여) 오늘이 아시안컵 16강이 있던 날인데, 축구를 보니 볼 차고 싶은 생각이 너무 들던 하루

mini-frontend.tistory.com

 

 

이번에는 실제 코드를 통해 로그인 세션에서 토큰이 어떻게 하면 처리되는지 알아보자.

 


 

토큰이 만료 등의 토큰 처리는 Axios 인터셉터(Interceptor)를 통해 어떻게 처리할지 설정해 두었다.

 

1. Axios 인터셉터(Interceptor)란?

Axios 인터셉터(Interceptor)는 Axios를 사용하여 HTTP 요청을 전송하거나 응답을 받기 전후에 특정 코드를 실행할 수 있는 기능이다. 이를 통해 개발자는 요청을 서버로 보내기 전에 요청을 가공하거나, 서버로부터 응답을 받은 후에 응답 데이터를 처리할 수 있다.

 

인터셉터는 요청 인터셉터와 응답 인터셉터 두 가지 유형이 있다. 

 

1) 요청 인터셉터

요청 인터셉터는 HTTP 요청을 서버로 보내기 전에 호출된다. 이를 사용하여 요청 데이터를 수정하거나 요청 헤더에 인증 토큰을 추가하는 등의 작업을 수행할 수 있다.

예를 들어, 모든 요청에 사용자의 인증 정보를 포함시켜야 할 경우, 요청 인터셉터를 사용하여 이 작업을 자동화할 수 있다.

 

2) 응답 인터셉터

응답 인터셉터는 서버로부터 응답을 받은 후에 호출된다. 이를 통해 응답 데이터를 가공하거나, 특정 HTTP 상태 코드에 대한 에러 처리를 수행할 수 있다.

 예를 들어, 인증 토큰이 만료되어 서버가 401 상태 코드를 반환하는 경우, 응답 인터셉터를 사용하여 사용자를 로그인 페이지로 리다이렉트 하거나 새로운 토큰을 요청하는 로직을 구현할 수 있다.

 

 


 

이제 코드를 살펴보자.

 

2-1. Axios 인스턴스 생성 및 기본 URL 설정

 

본격적으로 들어가지 전에 Axios 인스턴스를 통해 기본 설정을 해주자.

 Axios 인스턴스를 사용하면 API 호출을 위한 기본 설정을 미리 정의할 수 있으며, 공통적으로 사용되는 설정을 재사용할 수 있습니다. 예를 들어, API 서버의 기본 URL, 요청 헤더, 타임아웃 설정 등을 사전에 정의할 수 있다. 이러한 방식은 코드의 중복을 줄이고, 유지보수성을 향상시킨다.

import axios from "axios";

// baseURL 설정
export const URL = "http://localhost:8080";

const Instance = axios.create({
  baseURL: URL,
  // 아래와 같이 헤더를 설정 가능
  // headers: {
  //   'Content-Type': 'application/json',
  // },
});

 

코드에서는 axios.create 메소드를 사용하여 새로운 Axios 인스턴스를 생성하고, API 서버의 주소를 baseURL로 설정하였다.

 

2-2. 요청 인터셉터 설정

 

요청 인터셉터는 HTTP 요청이 서버로 전송되기 전에 호출된다. 이를 활용하여 모든 요청에 토큰을 자동으로 포함시킬 수 있다. (토큰을 담고 싶지 않다면 인터셉터를 사용하지 않으면 된다.)

// 요청 인터셉터: API 요청을 서버로 보내기 전에 실행
Instance.interceptors.request.use(
  (config) => {
  	// 세션 스토리지에서 'accessToken'을 가져옴
    const token = sessionStorage.getItem("accessToken");
    // 토큰이 존재하면, 요청 헤더에 인증 토큰을 추가
    if (token) {
      config.headers["authorization"] = `Bearer ${token}`;
    }
    return config;
  },
  (error) => 
    return Promise.reject(error);
  }
);

 

코드에서 사용되는 사용되는 로직 순서는 다음과 같다.

  1. 요청 인터셉터: API 요청을 서버로 보내기 전에 실행된다.
  2. 세션 스토리지에서 'accessToken'을 가져온다.
  3. 토큰이 존재하면, 요청 헤더에 인증 토큰을 추가한다.
  4. 수정된 설정으로 요청을 계속 진행한다.

 

2-3. 응답 인터셉터 설정

 

이제 정말로 본격적으로 응답 인터셉터를 통해 토큰이 만료되었을 때의 처리 방법을 코드를 통해 알아보고자 한다.

// 응답 인터셉터: 서버로부터의 응답을 받은 후 실행.
Instance.interceptors.response.use(
  (response) => {
    // 정상적인 응답을 처리하고 반환
    return response;
  },
  async (error) => {
    // 원본 요청 객체를 가져옴
    const originalRequest = error.config;
    // 오류 응답이 401 상태 코드이고, 해당 요청이 재시도되지 않았다면 새 토큰을 요청
    if (error.response.status === 401 && !originalRequest._retry) {
      // 요청을 재시도하기 위한 플래그를 설정
      originalRequest._retry = true;
      try {
        // 새 액세스 토큰을 요청하는 로직을 실행
        const response = await axios.post(`${URL}/retoken`, { /* 토큰 재발급 로직 */ });
        // 응답 헤더에서 새로 발급된 액세스 토큰을 가져옴
        const newAccessToken = response.headers["authorization"];

        // 새 액세스 토큰을 세션 스토리지에 저장
        sessionStorage.setItem("accessToken", newAccessToken);
        // 원본 요청의 헤더를 새 토큰으로 업데이트
        originalRequest.headers["authorization"] = `Bearer ${newAccessToken}`;

        // 수정된 원본 요청으로 API 호출을 재시도
        return axios(originalRequest);
      } catch (refreshError) {
      	// 리프레쉬 토큰이 만료될 경우, 로그아웃 처리
        if (refreshError.response?.data.code === "AUTH004") {
          alert(
            "로그인 세션이 만료되었습니다.\n다시 로그인 하시길 바랍니다."
          );
        }
        return Promise.reject(refreshError);
      }
    }
    // 그 외의 오류 응답을 외부로 전파
    return Promise.reject(error);
  }
);

 

응답 인터셉터에서 오류를 통해 토큰을 처리하는 과정을 순서대로 정리하자면 다음과 같다.

 

  1. 응답 인터셉터 설정: Axios 인스턴스에 응답 인터셉터를 설정한다. 이 인터셉터는 서버로부터 응답을 받은 후에 실행되며, 정상 응답과 오류 응답을 처리할 수 있다.
  2. 오류 응답 처리 시작: 서버로부터 오류 응답을 받았을 때 (예: 401 Unauthorized), 이를 처리하기 위한 로직이 시작된다.
  3. 원본 요청 객체 접근: error.config를 통해 오류가 발생한 원본 요청의 설정을 가져온다.
  4. 재시도 여부 확인: originalRequest._retry를 확인하여 해당 요청이 이미 재시도 되었는지 확인한다. 처음 오류가 발생했을 때는 이 값이 undefined 또는 false일 것이다.
    (originalRequest._retry는 요청이 이미 재시도되었는지 여부를 추적하는 데 사용되며, 응답 인터셉터 내에서 자동 토큰 갱신 로직을 처리할 때, 무한 재시도 루프에 빠지는 것을 방지하는 것이다.)
  5. 재시도 상태 설정: 해당 요청이 재시도되지 않았다면, originalRequest._retry = true를 설정하여 요청이 재시도됨을 표시한다.
  6. 새 토큰 요청 실행: axios.post(${URL}/retoken, {})를 호출하여 새로운 액세스 토큰을 요청한다.
  7. 새 액세스 토큰 처리: 새 토큰 요청이 성공하면, 응답으로부터 새로 발급된 액세스 토큰을 가져온다.
  8. 세션 스토리지에 토큰 저장: sessionStorage.setItem("accessToken", newAccessToken)를 사용하여 새 토큰을 세션 스토리지에 저장한다.
  9. 원본 요청 헤더 업데이트: originalRequest.headers["authorization"] = Bearer ${newAccessToken};를 사용하여 원본 요청의 헤더에 새 토큰을 설정한다.
  10. 원본 요청 재시도: return axios(originalRequest)를 호출하여 수정된 원본 요청을 서버로 다시 전송한다.
  11. 새 토큰 요청 중 오류 처리: 새 토큰을 요청하는 과정에서 오류가 발생한 경우, 이를 catch 블록에서 잡아 로그아웃 등으로 처리한다. 리프레쉬 토큰이 만료되었으면 로그아웃 처리를 해줄 수 있다.

 

 


 

 

이처럼 요청 인터셉터는 API 요청이 서버로 전송되기 전에 실행되어, 요청에 액세스 토큰을 추가한다. 이는 보안된 API 엔드포인트에 대한 인증을 자동화하는 데 유용하다.

응답 인터셉터는 서버로부터의 응답을 받은 후에 실행되며, 특히 인증 오류(401)에 대응하여 액세스 토큰을 자동으로 갱신하고 원래의 요청을 재시도하는 로직을 포함한다. 이 과정은 사용자 인터랙션 없이 세션을 유지하고, 필요한 경우 토큰을 새로 발급받는 자동화된 방식을 가능하게 한다.

 

이러한 방법은 애플리케이션 전반에 걸쳐 일관된 API 호출 패턴을 유지할 수 있으며, 인증 토큰 관리를 자동화할 수 있다. 요청 및 응답 인터셉터를 사용하면, 중복 코드를 줄이고, API 요청과 응답 처리를 중앙에서 관리할 수 있어, 보다 깔끔하고 유지보수가 용이한 코드베이스를 유지할 수 있다는 장점이 있다.