
흔히 JWT(JSON Web Tokens)을 사용하여 인가 & 인증 방식을 구현하며, Access Token과 Refresh Token을 사용합니다.
Access Token은 보호된 리소스에 접근하기 위해 사용되며, Refresh Token은 Access Token 재발급을 위해 사용합니다.
만약 만료되 Access Token과 함께 요청을 전달하면 서버에서는 만료된 토큰을 인지하여 클라이언트에세 401 에러를 전달합니다. 클라이언트에서는 401 에러를 응답받게 되면, Refresh Token을 사용하여 Access Token을 재발급받은 뒤 기존 요청을 다시 보내도록 해야 합니다.
Fetch API를 사용하여 만료된 토큰에 대한 Re-fetch 로직은 아래와 같습니다.
interface IRequestInit {
url: string
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
headers?: HeadersInit
body?: BodyInit | null
}
class Network {
// 토큰 재발급 여부
private isRefreshing = false
// 재발급 요청에 대한 Promise 객체
private refreshTokenPromise = null
// 실제 토큰은 인스턴스 프로퍼티로 관리하지 않습니다
private accessToken = ''
private refreshToken = ''
async request(requestInit: IRequestInit) {
try {
const {url, ...requestOptions} = requestInit
const response = await fetch(url, requestOptions)
// Fetch API는 요청을 실패한 에러에 대해서만 catch 하므로
// 서버가 응답한 에러에 대해서는 catch하지 않습니다.
// 직접 response.ok가 false인 경우 직접 에러를 throw 해주어야 합니다.
if(response.ok) return response
if(response.status === 401) {
return this.handleExpiredAccessToken(requestInit)
}
} catch(error) {
throw new Error(error.message)
}
}
// 토큰 만료시 토큰 재발급 후 기존 요청 re-fetch
private async handleExpiredAccessToken(requestInit: IReqeustInit) {
try {
const {url, requestOptions} = requestInit
if(!this.isRefreshing) {
this.isRefreshing = true
this.refreshTokenPromise = this.reIssuedAccessToken(this.refreshToken)
const { accessToken: reIssuedAccessToken } = await this.refreshTokenPromise
this.accessToken = reIssuedAccessToken
this.isRefreshing = false
} else if (this.refreshTokenPromise) {
await this.refreshTokenPromise
}
const headers = {
...requestOptions,
Authorization: `Bearer ${reIssuedAccessToken}`
}
const retryResponse = await fetch(url, requestOptions)
if(!retryResponse.ok) throw new Error(response.statusText)
return retryResponse
} catch(error) {
throw new Error(error.message)
}
}
// 토큰 재발급
private async getReIssuedAccessToken(refreshToken: string) {
try {
const response = fetch(url, options)
if(!response.ok) return throw new Error(response.statusText)
return response.json()
} catch(error) {
throw new Error(error.message)
}
}
}
중요한 것은 isRefreshing과 refreshTokenPromise 프로퍼티 입니다.
Promise.all을 사용한다면 여러 요청을 병렬적으로 동시에 전달하는데, isRefreshing과 refreshTokenPromise를 통해 동시에 여러 개의 요청이 동시에 토큰 갱신을 시도하는 상황에서 동기화를 보장하기 위함입니다.
토큰 갱신은 동일한 리프레시 토큰을 사용하여 한 번만 이루어져야 하고, 다른 요청들은 동시에 토큰을 갱신하지 않도록 해야합니다
isRefresing : 토큰 재발급 여부를 나타내는 프로퍼티로, 해당 프로퍼티가 false인 경우에만 reIssuedAccessToken을 호출하여 토큰을 갱신합니다. 토큰 갱신이 완료된 이후에는 false를 할당하여 이후 401 에러에 대해 다시 갱신 가능하도록 설정해야 합니다.
refreshTokenPromise : 토큰 재발급 요청을 나타내는 Promise 객체를 할당합니다. 만약 refreshTokenPromise에 Promise 객체가 존재한다면 이미 토큰을 재발급 받는 요청을 전달했다는 의미이며, Promise 객체의 상태가 fulfilled될 때까지 기다린다면 이미 재발급된 토큰을 갖고 있다는 의미를 갖게 됩니다.
즐겁게 읽었습니다. 유용한 정보 감사합니다.