로그인 구현 및 인터셉트

차차·2023년 1월 28일
0
post-thumbnail
  1. 최상위 파일에서 axios 디폴트 값을 세팅한다.
// index.js
import axios from "axios";

axios.defaults.withCredentials = true;
axios.defaults.baseURL = process.env.REACT_APP_SERVER_URL;

  1. 로그인하였을 때 accessToken을 headers에 디폴트로 세팅한다.
// 로그인 기능
  const onValidSubmit = useCallback(async (data: ILogin) => {
    const {
      data: { accessToken },
      status,
    } = await logIn(data);

    if (status !== 201) return alert("로그인 실패");
    axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
  }, []);

  1. Access Token을 재발급 받아 재요청하는 기능 추가.
// axios.ts

import { getRefreshToken } from "./cookie";

let isTokenRefreshing = false;

let refreshSubscribers: Array<(accessToken: string) => void> = [];

const addRefreshSubscriber = (callback: (accessToken: string) => void) => {
  refreshSubscribers.push(callback);
};

const onTokenRefreshed = (accessToken: string) => {
  refreshSubscribers.map((callback) => callback(accessToken));
};

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

    if (status === 401) { // 에러 내용 확인도 추가
      if (!isTokenRefreshing) {
        // isTokenRefreshing이 false인 경우에만 token refresh 요청
        isTokenRefreshing = true;
        const refreshToken = getRefreshToken();
        const { data } = await axios.post(
          `${process.env.REACT_APP_SERVER_URL}auth/refreshAccessToken`, // token refresh api
          {
            refreshToken,
          }
        );
        // 새로운 토큰 저장
        const { accessToken: newAccessToken } = data;
        isTokenRefreshing = false;
        axios.defaults.headers.common.Authorization = `Bearer ${newAccessToken}`;
        // 새로운 토큰으로 지연되었던 요청 진행
        onTokenRefreshed(newAccessToken);
      }

      // token이 재발급 되는 동안의 요청은 refreshSubscribers에 추가
      const retryOriginalRequest = new Promise((resolve) => {
        addRefreshSubscriber((accessToken: string) => {
          // 갱신한 accessToken으로 재설정
          originalRequest.headers.Authorization = "Bearer " + accessToken;
          // originalRequest를 다시 실행
          resolve(axios(originalRequest));
        });
      });
      return retryOriginalRequest;
    }
    return Promise.reject(error);
  }
);

export default instance;

이 방법으로 구현을 한다면 엑세스 토큰이 만료되었을 때 한 페이지에서 n만큼 요청을 보내는 상황에서 n번의 엑세스 토큰을 재발급하는 것이 아닌 한 번의 재발급으로 실패한 요청들을 한꺼번에 처리할 수 있다.



interceptor 설명

  1. 응답 에러가 발생하였을 때 그것이 인증에러라면 interceptor 안에 내용을 실행한다.
  2. isTokenRefreshingfalse일 경우에는 토큰 재발급을 실행.
    • 재발급 로직을 실행할 때 isTokenRefreshing 값을 true로 변경.
    • 토큰 재발급이 완료되면 다시 false로 변경.
    • axios의 헤더 디폴트 값으로 재발급 받은 엑세스 토큰 값을 설정
    • onTokenRefreshed(재발급 엑세스 토큰)을 실행.
      • 배열에 담긴 실패 요청들을 map()을 통해 재요청 보내는 함수.
  3. isTokenRefreshingtrue. 즉, 재발급이 진행 중일 때는 retryOriginalRequest를 통해 새로운 엑세스 토큰을 인자로 받아 재요청 보내는 함수를 addRefreshSubscriber에 전달.
    • addRefreshSubscriber 는 배열 안에 callback(accessToken)들을 refreshSubscribers 로 push()하는 함수.

요약

응답 에러가 발생하면 첫 번째 요청에 한해서 토큰 재발급을 실행하고 실패한 요청들을 배열에 담아 재발급이 완료되었을 때 한꺼번에 재요청을 실행한다.


개선사항

현재 isTokenRefreshing 이라는 boolean 값으로 첫 번째 요청이 아닌 것들은 재발급을 시키지 않고 있는데 callback 함수를 담은 배열 refreshSubscribers의 길이로 제어할 수 있을 것 같다는 조언이 있었다.

[현재]
if(!isTokenRefreshing){
	isTokenRefreshing = true;
	// 엑세스 토큰 재발급
	isTokenRefreshing = false;
}
// 실패요청 배열에 담기 

[개선]
// 실패요청 배열에 담기 
if(배열의 길이 <= 1){
	// 엑세스 토큰 재발급
}

이번 경험을 통해 Cookie / Storage와 보안에 관련된 지식들을 많이 알게되었다. 로그인 지금까지 쉽게 생각하고 있었는데 정말 엉망진창으로 만들었던거였구나… 아직 서버와 맞춰보지는 않았지만 될 것만 같은 너낌 😁



참고 블로그

🍪 프론트에서 안전하게 로그인 처리하기 (ft. React)

profile
나는야 프린이

0개의 댓글