JWT 토큰 기반의 인증을 프로젝트에 적용할 때, 클라이언트 사이드에서 신경써야 할 부분 중 하나가 바로 토큰 로테이션이다. JWT 토큰은 만료 기한이 반드시 존재하고, 토큰이 만료된 경우 새 토큰을 발급받기 위한 로직이 필요하다. 특히 Access 토큰과 Refresh 토큰을 함께 쓴다면 상황이 조금 더 복잡해진다. 이번 포스팅에서는 HTTP 클라이언트로 Axios를 사용하는 환경에서, Axios의 HTTP Interceptor 라는 괜찮은 미들웨어를 통해 어떻게 토큰 로테이션을 구현할 수 있을지 소개해보고자 한다.
만약 JWT, Access Token 과 Refresh Token 이 생소하다면 다음 블로그를 참고해보자.
보통 유저 인증이 필요한 API는 제한되어 있으나, 인증을 처리하는 로직은 공통으로 수행된다. 리소스를 수정하거나, 생성하는 등의 액션을 요구하는 요청은 인증 과정을 거쳐 본래 요청을 처리하게 된다. 유저의 권한이 없거나 토큰이 만료되는 등 인증에 실패하는 경우, 서버에서는 인증에 실패했다는 공통 응답을 제공할 것이다. 이때 HTTP 요청/응답 과정에서 Axios Interceptor 를 사용하면 클라이언트에서 인증을 관리하기 용이해진다. 이번에는 토큰 만료 시 서버에 토큰 갱신 요청을 통해 새로운 토큰을 발급받고, 원래 들어온 HTTP 요청을 다시 요청하는 로직이 필요한 경우를 가정하여 Axios API 재요청을 구현하는 방법을 소개해보고자 한다. 인증 처리 플로우는 다음과 같다.
Axios Interceptor 생성
인터셉터를 사용하기 위해서는 다음과 같이 Axios 인스턴스를 생성한 뒤, onRequest 와 onResponse 핸들러를 추가하면 된다.
import axios from 'axios';
const instance = axios.create();
// 요청 인터셉터
instance.interceptors.request.use(function (config) {
return config;
}, function (error) {
// 요청 오류 처리
return Promise.reject(error);
});
// 응답 인터셉터
instance.interceptors.response.use(async function (response) {
// 응답 데이터가 있는 작업 수행
// 2xx 범위에 있는 상태 코드일 경우 트리거
return response;
}, async function (error) {
// 응답 오류가 있는 작업 수행
// 2xx 외의 범위에 있는 상태 코드일 경우 트리거
return Promise.reject(error);
});
export default instance;
Request Interceptor
요청 인터셉터에서는 인증 헤더를 심어주면 모든 요청에 일일이 헤더를 심어주지 않아도 되어서 편리하다.
클라이언트 스토리지에서 토큰을 꺼내와서 헤더에 심어주는 코드는 다음과 같다. (포스팅 편의상 로컬 스토리지로 해두었지만, 프로젝트에서 관리하는 토큰 저장소에서 꺼내오는 걸로 치환해서 사용하면 된다. 보안상 브라우저 스토리지보다는, 메모리 또는 http only 쿠키 등으로 보다 안전하게 저장하는 것이 권장된다.)
// 요청 인터셉터
instance.interceptors.request.use(function (config) {
// 스토리지에서 토큰을 가져온다.
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
// 토큰이 있으면 요청 헤더에 추가한다.
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
// Refresh 토큰을 보낼 경우 사용하고자 하는 커스텀 인증 헤더를 사용하면 된다.
if (refreshToken) {
config.headers['x-refresh-token'] = refreshToken;
}
return config;
}, function (error) {
// 요청 오류 처리
return Promise.reject(error);
});
Response Interceptor
Response Interceptor 에서는 토큰 재인증, 자동 로그아웃 등 HTTP Response 에 따른 예외를 처리해주면 된다. Response 시나리오는 다음 세 가지로 분류해볼 수 있겠다.
1. 인증이 필요 없거나, 정상적으로 처리된 경우
이 경우 인터셉터에 별다른 예외처리가 필요 없다. 요청에 대한 응답을 가로챌 필요 없이 그대로 사용하면 된다.
2. 인증 정보가 없거나 잘못된 경우
401 상태 코드와 함께 토큰이 유효하지 않다는 에러 코드가 온다면, 로그아웃 처리를 통해 유저가 다시 로그인할 수 있도록 유도해야 한다. 응답 인터셉터는 다음과 같이 구성할 수 있다.
// 응답 인터셉터
instance.interceptors.response.use(async function (response) {
// 2xx 범위에 있는 상태 코드인 경우
return response;
}, async function (error) {
// 2xx 외의 범위에 있는 상태 코드인 경우
// 응답 오류가 있는 작업 수행
const {config, response: {status, data}} = error;
if (status === 401 && data.message === "InvalidTokenException") {
// 토큰이 없거나 잘못되었을 경우
logout();
}
return Promise.reject(error);
});
3. 토큰이 만료된 경우
엑세스 토큰이 만료된 경우, 클라이언트에서 가지고 있는 리프레시 토큰으로 엑세스 토큰을 재발급한 뒤, 갱신된 토큰으로 본래 요청을 다시 보내는 방법이 있다. 이때 리프레시 토큰까지 만료되었을 경우 토큰 갱신에 실패할 것이기 때문에 자동으로 로그아웃 처리 등 에러 핸들링을 해주는 것이 중요하다.
따라서 Response 를 가로채는 시점에 에러 코드를 확인하고 토큰을 재발급하는 로직이 다음과 같이 추가되어야 한다. 재발급 이후에는 원본 API 요청에 대한 config 를 가지고 재요청이 가도록 처리해볼 수 있겠다.
// 응답 인터셉터
instance.interceptors.response.use(async function (response) {
return response;
}, async function (error) {
const {config, response: {status}} = error;
if (status === 401 && data.message === "InvalidTokenException") {
// 토큰이 없거나 잘못되었을 경우
logout();
}
if (status === 401 && data.message === "TokenExpired") {
try {
const tokenRefreshResult = await instance.post('/refresh-token');
if (tokenRefreshResult.status === 200) {
const { accessToken, refreshToken } = tokenRefreshResult.data
// 새로 발급받은 토큰을 스토리지에 저장
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
// 토큰 갱신 성공. API 재요청
return instance(config)
} else {
logout();
}
} catch (e) {
logout();
}
}
return Promise.reject(error);
});
결론
이렇게 JWT 토큰을 사용하는 방법 중 하나인, Access Token & Refresh Token을 클라이언트에서 어떻게 사용하고 갱신해야 하는지 살펴보았다. 위에서 제시한 방법은 토큰 로테이션을 구현하는 한 가지 방식일 뿐이며, 반드시 인증 과정을 저렇게 구현해야 할 필요는 없다. 프론트엔드에서 직접 JWT를 디코딩하여 만료시간을 미리 계산하고, 만료되기 전에 미리 토큰을 갱신하는 방법도 있다. 중요한 것은 이러한 인증 처리를 Axios 에서 제공해주는 Interceptor를 통해 간편하게 구현할 수 있다는 것이다.
Axios Interceptor 는 클라이언트와 서버 통신 과정에서 쓸 수 있는 좋은 미들웨어다. 어플리케이션 전반적으로 사용되어야 하는 인증 처리를 한 가지 미들웨어로 몰아두면 구현 뿐 아니라 유지보수에도 편리하다. 앞으로도 인증 뿐 아니라 서버 통신과 관련된 예외처리, 캐싱, 재요청 등의 기능을 추가할 경우 인터셉터를 잘 활용해보면 좋겠다.
Reference
https://axios-http.com/docs/interceptors
https://tansfil.tistory.com/59
'Web' 카테고리의 다른 글
서로 다른 윈도우 간 안전하게 통신하는 방법 (0) | 2024.03.17 |
---|---|
헷갈리는 줄바꿈, 올바르게 제어해보자 (15) | 2023.12.10 |
JavaScript로 이미지 파일 데이터 다루기 (0) | 2023.08.31 |
[Web] DOM API를 효과적으로 사용해보자 (0) | 2022.04.16 |
브라우저 렌더링 (1) - 클라이언트 사이트 렌더링 (0) | 2022.01.26 |