현재 상황
나는 jwt방식을 사용하며
accessToken과 refreshToken을 모두 쿠키에 보관하고 있다.
모두 httpOnly, Secure 옵션을 주어 안전하게 사용하고자 했다.
- httpOnly: document.cookie를 통해 자바스크립트에서 접근을 막는다.
- Secure: HTTPS로 통신하는 경우에만 쿠키가 전송된다.
전체적인 큰 그림은 아래와 같다.
accessToken 재발급 시 refreshToken을 왜 갱신하느냐... 할 수도 있지만 서비스 상 로그인 상태를 최대한 오래 유지하기 위해 이렇게 진행했다. token을 어디에 저장하고 어떻게 관리할지에 대해 정답은 없고 주어진 상황에 맞게 사용하면 되는 거 같다.
axios 인스턴스 생성
프론트에서 쿠키를 직접 다룰 수 없어(httpOnly옵션 때문에) axios 인스턴스를 생성하고 withCredentials 옵션을 true로 설정해 줬다. 이렇게 하면 api 요청 시마다 쿠키값들이 같이 보내진다.
추가로 baseURL에 기본적으로 사용할 백앤드 api url도 넣어줬다
const axiosT = axios.create({
baseURL: `${API_HOST}`,
withCredentials: true,
});
axios interceptor로 accessToken 재발급받기
axios interceptor
request과 response를 가로채 특정 작업을 수행할 수 있게 해 준다!
나는 interceptor에서 response를 활용하였고 전체적인 재발급 과정은 다음과 같다.
1. response를 가로채 401 Error(권한 없음) 발생 시 accessToken 재발급받기 위해 api 요청(refreshToken을 이용해 accessToken 재발급)을 해줬다.
2. 정상적으로 accessToken 재발급이 되었다면 이전에 실패했던 요청을 다시 하면 끝이다! 만약 refreshToken도 만료되었다면 '다시 로그인해 주세요.'라는 alert과 함께 로그아웃 시켰다.
httpClient.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const { config, response } = error
if (response && response.status === 401) {
await httpClient.post('/v1/auth/refresh')
httpClient(config)
}
return Promise.reject(error)
},
)
export default httpClient
다중 요청 시 accessToken 재발급 중복 요청 문제
이제 잘 된 줄 알고 기뻤다ㅎㅎ 별거 아니네~하고 계속 작업을 하는데
마이페이지에서 accessToken이 필요한 api를 2개 요청하는데 첫 번째 요청에서 토큰이 재발급하는 동안 두 번째 요청도 토큰이 만료되어 같이 accessToken 재발급 요청을 하고 있는 것이다!!! 🤣🤣
이게 왜 문제냐! 하면
백앤드에서는 accessToken을 재발급할 때 refreshToken도 갱신해서 redis에 보관하고 있었다. 클라이언트에서 accessToken 요청을 여러 개 보냈을 경우 요청을 보낸 순차적으로 응답을 받는다는 보장이 없어 최신의 refreshToken이 아닌 이전의 refreshToken이 저장될 수 있다.
다시 말해, redis에 저장된 refreshToken과 클라이언트에 최종적으로 전달된 refreshToken이 다를 수 있다는 말이다!
이를 해결하기 위해 accessToken이 재발급되고 있는 동안 다른 요청이 들어올 경우 배열에 넣어두었다가 재발급 이후에 배열에 넣어둔 요청들을 보냈다!
전체소스
import axios from 'axios'
const httpClient = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_API_HOST}`,
withCredentials: true,
})
let isTokenRefreshing = false
let refreshSubscribers: (() => void)[] = []
const onTokenRefreshed = () => {
refreshSubscribers.forEach((callback) => callback())
}
const addRefreshSubscriber = (callback: () => void) => {
refreshSubscribers.push(callback)
}
httpClient.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const { config, response } = error
if (response && response.status === 401) {
// token이 재발급 되는 동안의 요청은 refreshSubscribers에 저장
const retryOriginalRequest = new Promise((resolve) => {
addRefreshSubscriber(() => {
resolve(httpClient(config))
})
})
if (!isTokenRefreshing) {
isTokenRefreshing = true
try {
await httpClient.post('/v1/auth/refresh')
isTokenRefreshing = false
onTokenRefreshed()
refreshSubscribers = []
} catch (e) {
isTokenRefreshing = false
refreshSubscribers = []
}
}
return retryOriginalRequest
}
return Promise.reject(error)
},
)
export default httpClient
참고자료
'IT > JavaScript' 카테고리의 다른 글
모바일 웹에서 100vh 적용하기 (0) | 2023.11.01 |
---|---|
모바일 웹에서 app으로 이동(딥링크) (0) | 2023.10.26 |
web Components로 bottomsheet 만들기 (0) | 2023.02.19 |
자바스크립트에서 뒤로가기 감지하기 (0) | 2022.06.07 |
slice와 splice의 차이점 (0) | 2022.04.05 |