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 관리.
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 secureStoragesrc/repositories/TokenRepository.ts에 key에 따른 저장 데이터를 따로 repository를 통해 CRUD를 다시 관리
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 tokenRepositoryUsage
Todo
- retry 구현 필요.
- 여러 요청이 async로 동잘 될 경우, 재발급 시도를 여러 번 할 수 있는 문제 대책 강구
- Axios interceptor를 통해 request 시점에, TokenRepository에서 accssToken을 불러와, request Header에 삽입
- response 시점에 interceptor를 통해 401 ERROR 발생을 감지하여, refreshToken을 통해 재발급 시도 → 재발급에 성공한 경우, 기존에 실패한 request를 retry
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