1. JWT
JWT와 같은 Bearer 토큰은 access token만으로도 서버에서 디지털 서명을 통해 위변조 확인 후 사용자 인가를 처리할 수 있다. 그러나 탈취 가능성을 고려하여 access token의 만료 기간을 짧게 설정하여 탈취 가능성을 낮추는데, 클라이언트는 access token의 짧은 만료 기간 때문에 자주 발급받아야 하는 불편함이 있다. 이 문제를 해소하기 위해서 사용하는 것이 refresh token이다. refresh token을 사용하면 access token보다 만료 기간이 길어 access token이 만료되었다면 refresh token으로 access token을 갱신하도록 돕고 access token의 stateless한 특성에서 발생하는 취약점을 보완할 수 있다.
2. refresh token으로 access token 갱신하기
access token과 refresh token을 사용하기 위해서는 refresh toekn과 access token을 갱신하는 방식을 알아야 한다.
access token 갱신
1. 클라이언트에서 access token을 확인하고 만료가 임박하면 request 헤더에 refresh token 을 넣어 보낸다.
- 클라이언트는 매 API 요청 시 access token의 만료 시간을 확인하고 refresh token을 실어보내고 서버에서 온 응답에 access token이 존재하는지 확인해야 한다.
- 서버에서는 refresh token의 여부를 확인하고 존재하면 갱신된 access token을 보내주어야 한다.
2. 클라이언트에서 access token을 확인하고 만료가 임박하면 refresh token을 보낸 후 갱신된 access token으로 요청을 보내는 방식
1번 방식은 리프레시 토큰과 함께 보내려는 요청을 보내는 반면, 이 방식은 리프레시 요청을 보내고 나서 받은 갱신된 액세스 토큰으로 보내려는 요청을 보내는 방식이다.
3. 클라이언트에서 API 요청을 보낸 후에 서버에서 만료 여부를 응답하고, 응답에 따라 refresh token을 보내고 갱신된 access token으로 요청을 보내는 방식
access token과 refresh token을 헤더에 넣어 서버로 요청을 보내면, 서버에서 access token의 만료 여부를 판단한다. 그리고 401 코드가 반환되면, refresh 요청을 보내고 서버로부터 갱신된 access token을 받아 기존 요청을 보낸다.
현재 내가 구현한 방식에는 3번 방식을 채택했다. API 요청이 많아지긴 하지만 클라이언트에서 복잡한 구현 없이 직관적으로 처리할 수 있어 이 방식을 사용했다.
refresh token의 갱신 여부 판단
1. access token만 갱신하는 방식
2. refresh 요청 후 받은 refresh token의 유효기간이 얼마남지 않았을 때, access token과 refresh token을 갱신
현재는 1번 방식으로 refresh token의 만료 여부를 401 코드로 반환하며 새롭게 로그인하도록 구현되어있다.
3. Axios 인터셉터로 구현
Axios 인터셉터를 활용하면 response와 request를 가로채서 필요한 작업을 추가할 수 있다. 이러한 작업을 통해서 서버로 보내는 모든 요청에 자동으로 사용자 인증 정보를 포함시키고 액세스 토큰이 만료되었다면 새로운 토큰을 받아서 재시도할 수 있게 한다.
constructor() 설정
먼저, AxiosClient라는 클래스를 안에 axiosInstance 객체를 생성했다. baseUrl, header 값을 넣고 axios를 생성한다.
class AxiosClient {
private axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = axios.create({
baseURL: url,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
}
Request Interceptor
getTokens 메서드를 사용해서 asyncStorage에 저장된 토큰이 있는지 확인한다. 있다면 헤더의 Authorization 필드에 액세스 토큰을 넣어 반환한다.
this.axiosInstance.interceptors.request.use(
async config => {
const tokens = await this.getTokens();
if (tokens && tokens.accessToken) {
config.headers['Authorization'] = `Bearer ${tokens.accessToken}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
private async getTokens(): Promise<any> {
const tokensString = await AsyncStorage.getItem('Tokens');
return tokensString ? JSON.parse(tokensString) : null;
}
Response Interceptor
서버로부터 온 응답을 가로채서, response의 status 값이 401인지 확인한다. 401이면 액세스 토큰이 만료되었다는 의미이므로 refresh 요청을 보내어 갱신된 액세스 토큰으로 설정해준다.
this.axiosInstance.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (
error.response.status === 401 &&
error.response.data.message === '만료된 토큰입니다.' &&
!originalRequest._retry
) {
originalRequest._retry = true;
const newTokens = await this.refreshToken();
if (newTokens) {
axios.defaults.headers.common['Authorization'] =
`Bearer ${newTokens.accessToken}`;
return this.axiosInstance(originalRequest);
}
}
return Promise.reject(error);
}
);
refreshToken 메서드를 자세히 보면, asyncStorage에 토큰이 존재하지 않으면 refresh 요청을 보내어 새로운 refresh token을 받아 asyncStorage에 저장한다. 만약에 서버로부터 401 상태 코드를 받으면 기존의 토큰을 삭제하고 로그인 화면으로 리다이렉트 되는 플로우다.
private async refreshToken(): Promise<any> {
const tokens = await this.getTokens();
if (!tokens || !tokens.refreshToken) return null;
try {
const response = await this.axiosInstance.post('/auth/token/refresh', {
refreshToken: tokens.refreshToken,
});
const newTokens = response.data;
await AsyncStorage.setItem('Tokens', JSON.stringify(newTokens));
console.log('Token refreshed:', newTokens);
return newTokens;
} catch (error) {
console.error('Failed to refresh token:', error);
if (
error.response &&
error.response.status === 401 &&
error.response.data.message === '만료된 토큰입니다.'
) {
await this.handleExpiredRefreshToken();
}
return null;
}
}
private async handleExpiredRefreshToken(): Promise<void> {
await AsyncStorage.removeItem('Tokens');
// TODO : 로그인 화면으로 리다이렉트 + 전역 상태 업데이트
// navigation.navigate('Login);
Alert.alert('세션이 만료되었습니다. 다시 로그인해 주세요.');
}
이러한 방식으로 모든 API 요청에 인증 정보를 담아 보내도록 설정하고 토큰의 만료 여부를 확인할 수 있다.
예시
request 메서드를 통해 API 요청 시 Axios 에러에 대한 코드 중복을 줄이고 일관된 에러 및 응답 처리를 할 수 있도록 설정하였다.
private async request<T>(config: AxiosRequestConfig): Promise<T> {
try {
const response: AxiosResponse<T> = await this.axiosInstance(config);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
throw new Error(error.response.data.message || 'Request error');
}
throw new Error('네트워크 오류가 발생했습니다.');
}
}
public get<T>(endpoint: string, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>({...config, method: 'GET', url: endpoint});
}
public delete<T>(endpoint: string, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>({...config, method: 'DELETE', url: endpoint});
}
'FrontEnd > React-native' 카테고리의 다른 글
react-native 애플리케이션 성능 개선하기(feat.useQuery) (0) | 2024.08.22 |
---|---|
중첩 네비게이션에서 발생하는 Stack 초기화 문제 해결(feat.CommonAcitons) (0) | 2024.08.22 |
스켈레톤 화면이 사용자 경험 개선에 도움이 될까 (0) | 2024.08.04 |