Info

  • JWT token 사용.
  • API 별 권한의 필요 여부는 FE에서는 관리하지 않고 accessToken이 있으면 무조건 API 요청 시, authorization에 담아서 제공.
  • accessToken과 refreshToken은 react-secure-storage를 통해 관리.

React-secure-storage

  • 암호화 알고리즘을 적용하여 데이터를 안전하게 저장.
  • 그럼에도 Browser 기반으로 데이터를 관리하기 때문에 저장에는 유의해야 함.
    JWT의 경우에는 만료 시간이 정해져 있기 때문에 secure storage에 저장해도 큰 문제가 없음.
  • src/utils/storage/secureStorage.ts 를 통해 기본 CRUD function 관리.
secureStorage.ts
import secureLocalStorage from 'react-secure-storage'  
  
const secureStorage = {  
  get: (key: string) => secureLocalStorage.getItem(key),  
  
  set: (key: string, value: string | number | object | boolean | null) => {  
    if (value != null) secureLocalStorage.setItem(key, value)  
  },  
  
  drop: (key: string) => secureLocalStorage.removeItem(key)  
}  
  
export default secureStorage
  • src/repositories/TokenRepository.ts에 key에 따른 저장 데이터를 따로 repository를 통해 CRUD를 다시 관리
TokenRepository.ts
import { Token } from '@typings/Token'  
import secureStorage from '@utils/storage/secureStorage'  
  
type TokenRepository = {  
  getAccessToken: () => string | null  
  getRefreshToken: () => string | null  
  setAccessToken: (accessToken: string | null) => void  
  setRefreshToken: (refreshToken: string | null) => void  
  setToken: (token: Token) => void  
  getToken: () => Token | null  
  dropToken: () => void  
}  
const _accessTokenKey = 'ACCESS_TOKEN'  
const _refreshTokenKey = 'REFRESH_TOKEN'  
  
const tokenRepository: TokenRepository = {  
  getAccessToken: () => secureStorage.get(_accessTokenKey) as string,  
  
  getRefreshToken: () => secureStorage.get(_refreshTokenKey) as string,  
  
  setAccessToken: (accessToken: string | null) => secureStorage.set(_accessTokenKey, accessToken),  
  
  setRefreshToken: (refreshToken: string | null) => secureStorage.set(_refreshTokenKey, refreshToken),  
  
  setToken: (token: Token) => {  
    tokenRepository.setAccessToken(token.accessToken)  
    tokenRepository.setRefreshToken(token.refreshToken)  
  },  
  
  getToken: () => {  
    const accessToken = tokenRepository.getAccessToken()  
    const refreshToken = tokenRepository.getRefreshToken()  
    if (!accessToken || !refreshToken) return null  
    return {  
      accessToken: accessToken,  
      refreshToken: refreshToken  
    }  
  },  
  
  dropToken: () => {  
    secureStorage.drop(_accessTokenKey)  
    secureStorage.drop(_refreshTokenKey)  
  }  
}  
  
export default tokenRepository

Usage

Todo

  • retry 구현 필요.
  • 여러 요청이 async로 동잘 될 경우, 재발급 시도를 여러 번 할 수 있는 문제 대책 강구
  • Axios interceptor를 통해 request 시점에, TokenRepository에서 accssToken을 불러와, request Header에 삽입
  • response 시점에 interceptor를 통해 401 ERROR 발생을 감지하여, refreshToken을 통해 재발급 시도 재발급에 성공한 경우, 기존에 실패한 request를 retry
api.ts
import axios, { AxiosInstance } from 'axios'  
import tokenRepository from '@repositories/TokenRepository'  
import ApiError from '@utils/error/ApiError'  
import { apiCode } from '@utils/error/constant/ApiCode'  
  
export const api: () => AxiosInstance = () => {  
  const { getAccessToken } = tokenRepository  
  
  const instance: AxiosInstance = axios.create({  
    baseURL: import.meta.env.VITE_API_BASE_URL || '',  
    timeout: 5000,  
  })  
  
  instance.interceptors.request.use(  
    config => {  
      const accessToken = getAccessToken()  
  
      if (accessToken != null) config.headers['authorization'] = `Bearer ${accessToken}`  
  
      console.debug(  
        `\n[\x1B[34mREQUEST\x1B[0m]\n\n` +  
          `url : ${config.baseURL}/${config.url}\n` +  
          `method : ${config.method}\n\n` +  
          `headers : ${JSON.stringify(config.headers, null, 2)}\n` +  
          `body : ${JSON.stringify(config.data, null, 2)}\n` +  
          `queryParams : ${config.params}\n\n`,  
      )  
  
      return config  
    },  
  
    error => {  
      return Promise.reject(error)  
    },  
  )  
  
  instance.interceptors.response.use(  
    response => {  
      console.debug(  
        `\n[\x1B[31mRESPONSE\x1B[0m]\n\n` +  
          `url : ${response.config.baseURL}/${response.config.url}\n` +  
          `method : ${response.config.method}\n\n` +  
          `headers :${JSON.stringify(response.headers, null, 2)}\n` +  
          `body : ${JSON.stringify(response.data, null, 2)}\n\n`,  
      )  
      return response  
    },  
    error => {  
      if (!axios.isAxiosError(error)) return Promise.reject(error)  
  
      switch (error.response?.status) {  
        case 401:  
          return Promise.reject(new ApiError(apiCode.AUTH_ERROR, error))  
        default:  
          return Promise.reject(new ApiError(apiCode.UNKNOWN_ERROR, error))  
      }    },  
  )  
  return instance  
}  
  
export default api