주뇽's 저장소

[React] 리프레쉬 토큰을 이용한 액세스 토큰 재발급 방법 본문

웹개발/React

[React] 리프레쉬 토큰을 이용한 액세스 토큰 재발급 방법

뎁쭌 2024. 6. 12. 22:42
728x90
반응형

웹 애플리케이션에서 사용자를 인증하기 위해 토큰 기반 인증을 사용한다. 이때 액세스 토큰리프레쉬 토큰을 활용하여 보다 안전하고 효율적인 인증 시스템을 구축할 수 있다. 이 글에서는 리프레쉬 토큰의 필요성과 이를 활용한 액세스 토큰 재발급 방법을 상세히 설명한다.

 

1. 리프레쉬 토큰의 필요성

액세스 토큰은 사용자가 인증된 상태임을 나타내는 짧은 수명의 토큰이다. 이는 서버에 부담을 주지 않고 빠른 인증을 가능하게 하지만, 짧은 수명으로 인해 자주 만료될 수 있다. 액세스 토큰이 만료될 때마다 사용자를 다시 로그인시키는 것은 매우 불편하다. 이를 해결하기 위해 리프레쉬 토큰을 사용한다.

리프레쉬 토큰은 더 긴 수명을 가지며, 만료된 액세스 토큰을 재발급받을 수 있는 권한을 부여한다. 리프레쉬 토큰을 사용하면 사용자는 로그인 상태를 유지하면서 액세스 토큰을 갱신할 수 있다.

 

2. 인터셉터 설정

Axios 인터셉터는 모든 요청 또는 응답을 가로채서 특정 로직을 수행할 수 있게 해준다. 여기서는 요청 인터셉터와 응답 인터셉터를 설정하여 모든 API 요청에 자동으로 토큰을 추가하고, 만료된 토큰을 재발급받는 로직을 구현한다.

api/ApiClient.js 파일

먼저 Axios 인스턴스를 생성하고, 요청과 응답을 가로채는 인터셉터를 설정한다.

import axios from 'axios';
import { postRefreshTokenApi } from '../api/UserApi';

export const apiClient = axios.create({
  baseURL: 'https://your-api-base-url.com',
  withCredentials: true, // 쿠키를 포함시키기 위해 설정한다
});

export const setAuthToken = (token) => {
  if (token) {
    apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  } else {
    delete apiClient.defaults.headers.common['Authorization'];
  }
};

 

setupInterceptors 함수는 Axios 인스턴스에 요청 인터셉터와 응답 인터셉터를 설정한다.

export const setupInterceptors = (token, setToken, logout) => {
  // 요청 및 응답 인터셉터 구현
  	});

 

3. 요청 인터셉터

요청 인터셉터는 모든 요청이 서버로 보내지기 전에 실행된다. 여기서는 요청 헤더에 액세스 토큰을 자동으로 추가한다.

apiClient.interceptors.request.use(
  config => {
    if (!config.headers['Authorization']) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

 

4. 응답 인터셉터

응답 인터셉터는 서버로부터 응답을 받은 후 실행된다. 여기서는 응답 상태가 401(Unauthorized)일 경우, 리프레쉬 토큰을 사용하여 새로운 액세스 토큰을 받아오는 로직을 구현한다.

apiClient.interceptors.response.use(
  response => response,
  async (error) => {
    const prevRequest = error?.config;
    if (error?.response?.status === 401 && !prevRequest?.sent) {
      prevRequest.sent = true;
      try {
        const response = await postRefreshTokenApi();
        if (response.status === 200) {
          const newAccessToken = response.headers['access'];
          setToken(newAccessToken);
          apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
          prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
          return apiClient(prevRequest);
        } else {
          logout();
          throw new Error('Failed to refresh access token');
        }
      } catch (err) {
        logout();
        return Promise.reject(err);
      }
    }
    return Promise.reject(error);
  }
);

5. 리프레쉬 토큰을 사용한 액세스 토큰 재발급 과정

리프레쉬 토큰을 사용하여 새로운 액세스 토큰을 받아오는 과정은 다음과 같다:

  1. 401 에러 발생: 서버로부터 401(Unauthorized) 응답이 오면, 액세스 토큰이 만료되었음을 의미한다.
  2. 리프레쉬 토큰으로 요청: postRefreshTokenApi 함수를 호출하여 리프레쉬 토큰을 사용해 새로운 액세스 토큰을 요청한다.
  3. 새로운 액세스 토큰 저장: 응답이 성공적이면 새로운 액세스 토큰을 저장하고, 요청 헤더에 추가한다.
  4. 요청 재시도: 만료된 액세스 토큰으로 실패한 요청을 새로운 토큰으로 다시 시도한다.

6. AuthProvider에서 인터셉터 설정

AuthProvider 컴포넌트에서 setupInterceptors 함수를 호출하여 인터셉터를 설정한다.

import { createContext, useContext, useState, useEffect } from 'react';
import {jwtDecode} from 'jwt-decode';
import { postAuthLoginApi, postRefreshTokenApi } from '../api/UserApi';
import { apiClient, setAuthToken, setupInterceptors } from '../api/ApiClient';

export const AuthContext = createContext();
export const useAuth = () => useContext(AuthContext);

export default function AuthProvider({ children }) {
  const [isAuthenticated, setAuthenticated] = useState(false);
  const [username, setUsername] = useState(null);
  const [token, setToken] = useState(null);

  useEffect(() => {
    const savedToken = localStorage.getItem('token');
    if (savedToken) {
      setToken(savedToken);
      setAuthenticated(true);
      const decodedToken = jwtDecode(savedToken);
      setUsername(decodedToken.nickname);
      setAuthToken(savedToken);  // 토큰 설정
      setupInterceptors(savedToken, setToken, logout); // 인터셉터 설정
    }
  }, []);

  useEffect(() => {
    if (token) {
      localStorage.setItem('token', token);
      setAuthToken(token);  // 토큰 설정
    } else {
      localStorage.removeItem('token');
      setAuthToken(null);  // 토큰 제거
    }
  }, [token]);

  useEffect(() => {
    if (token) {
      const decodedToken = jwtDecode(token);
      const exp = decodedToken.exp * 1000;
      const timeout = exp - Date.now() - 60000;

      const timer = setTimeout(() => {
        refreshAuthToken();
      }, timeout);

      return () => clearTimeout(timer);
    }
  }, [token]);

  async function login(UserReqDto) {
    try {
      const response = await postAuthLoginApi(UserReqDto);
      if (response.status === 200) {
        console.log('로그인 성공');
        const token = response.headers['access'];
        if (token) {
          setToken(token);
          setAuthenticated(true);

          const decodedToken = jwtDecode(token);
          const nickname = decodedToken.nickname;
          setUsername(nickname);
          setAuthToken(token);  // 토큰 설정
          setupInterceptors(token, setToken, logout); // 인터셉터 설정
          return true;
        }
      } else {
        logout();
        return false;
      }
    } catch (error) {
      console.log(error);
      logout();
      return false;
    }
  }

  function logout() {
    setAuthenticated(false);
    setToken(null);
    setUsername(null);
    setAuthToken(null);  // 토큰 제거
  }

  async function refreshAuthToken() {
    try {
      const response = await postRefreshTokenApi();
      if (response.status === 200) {
        const newToken = response.headers['access'];
        setToken(newToken);
        setAuthenticated(true);
        setAuthToken(newToken);
      } else {
        logout();
      }
    } catch (error) {
      console.log(error);
      logout();
    }
  }

  const value = {
    isAuthenticated,
    login,
    logout,
    username,
    token,
    setToken
  };

  return (
    <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  );
}

'웹개발 > React' 카테고리의 다른 글

[React] 컴포넌트  (0) 2023.12.12
#10 로그아웃 기능 만들기(ReactJS)  (0) 2023.01.08
#9 Auth 기능 추가(ReactJS)  (0) 2023.01.08