react에서 axios.interceptors를 이용해 accessToken, refreshToken으로 로그인 구현하기(feat. @reduxjs/toolkit)

Maliethy·2021년 7월 25일
2

react

목록 보기
2/7

1. accessToken과 refreshToken을 브라우저 쿠키 저장소에 담아두기

백서버에서 온 accessToken과 refreshToken을 브라우저 cookie에 담기 위해 react-cookies라이브러리를 사용했다.
/utils/setToken.ts

import reactCookies from 'react-cookies';

let savedRefreshTokenExpireTime = [];
const onlyHTTPS = process.env.NODE_ENV === 'development' ? false : true;

function setToken(
  accessToken: string,
  refreshToken: string,
  omsID: string,
  accessTokenExpireTime: string,
  refreshTokenExpireTime?: string,
) {
  if (refreshTokenExpireTime) {
    savedRefreshTokenExpireTime.push(new Date(refreshTokenExpireTime));
  }

  reactCookies.save('accessToken', accessToken, {
    path: '/',
    expires: new Date(accessTokenExpireTime),
    secure: onlyHTTPS,
  });
  reactCookies.save('refreshToken', refreshToken, {
    path: '/',
    expires: savedRefreshTokenExpireTime[0],
    secure: onlyHTTPS,
  });
  
}

export default setToken;

/redux/reducers/user.ts

 .addCase(PostSignIn.fulfilled, (state, action) => {
        state.PostSignInLoading = false;
        state.me = action.payload;
        state.PostSignInDone = true;

        const accessToken = action.payload.accessToken;
        const refreshToken = action.payload.refreshToken;
       
        const accessTokenExpireTime = action.payload.accessTokenExpireTime;
        const refreshTokenExpireTime = action.payload.refreshTokenExpireTime;

        setToken(accessToken, refreshToken,accessTokenExpireTime, refreshTokenExpireTime);
      })

2. axiosApiInstance와 axiosApiRefreshToken만들기

/redux/actions/index.ts

import reactCookies from 'react-cookies';
import axios from 'axios';
import setToken from '@utils/setToken';
import { message } from 'antd';
import router from 'next/router';

const baseURL = process.env.NODE_ENV === 'development' ? '/' : 'https://example.co.kr/'; 

export const axiosApiInstance = axios.create({
  baseURL: baseURL,
  withCredentials: true,
});

export const axiosApiRefreshToken = axios.create({
  baseURL: baseURL,
  withCredentials: true,
});

axiosApiInstance.interceptors.request.use(
  async (config) => {
    const accessTokenByCookies = await Promise.resolve(reactCookies.load('accessToken'));

    config.headers = {
      Authorization: `Bearer ${accessTokenByCookies}`,
      Accept: 'application/json',
    };
    return config;
  },
  (error) => {
    Promise.reject(error);
  },
);

axiosApiRefreshToken.interceptors.request.use(
  async (config) => {
    const refreshTokenByCookies = await Promise.resolve(reactCookies.load('refreshToken'));

    config.headers = {
      Authorization: `Bearer ${refreshTokenByCookies}`,
      Accept: 'application/json',
    };
    return config;
  },
  (error) => {
    Promise.reject(error);
  },
);

axiosApiInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async function (error) {
    const originalRequest = error.config;
    const refreshTokenByCookies = await Promise.resolve(reactCookies.load('refreshToken'));

    if (error.response?.status === 401 && originalRequest.url === '/Web/RefreshToken') {
      console.log('Prevent infinite loops');
      return Promise.reject(error);
    }

    if (error.response?.status === 401 && refreshTokenByCookies) {
      try {
        if (refreshTokenByCookies) {
          const response = await axiosApiRefreshToken.get('/Web/RefreshToken');
          const newAccessToken = response.data.accessToken;

          setToken(newAccessToken, refreshTokenByCookies,  response.data.accessTokenExpireTime);
          originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;

          return axios(originalRequest);
        } else {
          // console.log('refreshToken 만료1 - access갱신 후에도 에러');
          reactCookies.remove('accessToken', { path: '/' });
          reactCookies.remove('refreshToken', { path: '/' });
        
          router.replace('/');
          message.warn('세션이 만료되어 다시 로그인이 필요합니다.', 4);
        }
      } catch (error) {
        // console.log('refreshToken 만료2 - 다른 기기 로그인');
        reactCookies.remove('accessToken', { path: '/' });
        reactCookies.remove('refreshToken', { path: '/' });

        router.replace('/');
        message.warn('다른 기기에서 로그인되었습니다. 다시 로그인해주세요.', 4);
      }
    }
    return Promise.reject(error);
  },
);

3. axiosApiInstance 사용하기

/redux/actions/user.ts

import { axiosApiInstance } from './index';
export const GetMyInfo = createAsyncThunk<
  GetMyInfoResponse,
  GetMyInfoRequest,
  {
    dispatch: AppDispatch;
  }
>('Web/GetMyInfo', async (data, { rejectWithValue }) => {
  try {
    const response = await axiosApiInstance.get(`/Web/GetMyInfo`);
    return response.data as GetMyInfoResponse;
  } catch (error) {
    return rejectWithValue(error.statusCode);
  }
});

번외. nextjs getServerSideProps에서는 따로 axios에 accessToken, refreshToken만료 로직을 만들어 사용하기

/pages/order.tsx

import wrapper from '@redux/store/configureStore';
import { GetServerSideProps } from 'next';
import nextCookies from 'next-cookies';
import axios from 'axios';
import orderSlice from '@redux/reducers/order';

export const getServerSideProps: GetServerSideProps = wrapper.getServerSideProps(async (context) => {
  const allCookies = nextCookies(context);
 
  const accessTokenByCookies = allCookies['accessToken'];
  const refreshTokenByCookies = allCookies['refreshToken'];

  const baseURL = 'https://oms.modument.co.kr/';
  axios.defaults.baseURL = baseURL;
  axios.defaults.headers.Authorization = `Bearer ${accessTokenByCookies}`;
  await context.store.dispatch(orderSlice.actions.redirectDuringSSR(true));

  if (accessTokenByCookies) {
    await context.store.dispatch<any>(
      GetOrderLists({
                 ...
      }),
    );
  } else {
    try {
      axios.defaults.headers.Authorization = `Bearer ${refreshTokenByCookies}`;
      const response = await axios.get('/Web/RefreshToken');
      const newAccessToken = response.data.accessToken;

      axios.defaults.headers.Authorization = `Bearer ${newAccessToken}`;
      await context.store.dispatch<any>(
        GetOrderLists({
               ...
        }),
      );
    } catch (error) {
      return {
        redirect: {
          destination: '/',
          statusCode: 301,
        },
      };
    }
  }

  return {
    props: {},
  };
});
profile
바꿀 수 있는 것에 주목하자

0개의 댓글