이전 프로젝트들은 모두 로그인하면 AccessToken만 받아 session storage에 넣어두고 이후 이루어지는 api 요청에 사용했었다.
그러다보니 AccessToken이 만료되면 무조건 로그인을 다시 해야 했다.
개발하는 단계에서는 편의성을 위해 만료 기간을 길게 잡아도 상관은 없으나 AccessToken에는 인증 정보가 모두 담겨있기 때문에 실제 서비스 단계에서는 AccessToken의 만료 기간을 길게 잡으면 위험하다.
그렇다고 사용자가 10분 간격으로 로그인을 다시 하게 할 수는 없기 때문에 이번 프로젝트에서는 RefreshToken을 도입하기로 했다.
RefreshToken은 AccessToken을 재발급하기 위한 token이다.
AccessToken의 만료 기간을 짧게 가져가는 대신 RefreshToken을 함께 사용하면서 AccessToken이 만료되면 RefreshToken으로 AccessToken을 재발급 받는다.
이러한 방식을 사용하면 AccessToken이 만료되었을 때 RefreshToken이 존재한다면 로그인 과정을 다시 요구할 필요 없이 로그인 상태를 연장시킬 수 있다.
로그인 처리에 RefreshToken + AccessToken 조합을 사용하면 예상할 수 있는 시나리오는 다음과 같다.
1. RefreshToken 유효 + AccessToken 유효
=> 정상적으로 로그인이 유지되는 경우.
2. RefreshToken 유효 + AccessToken 만료
=> AccessToken이 만료되었지만 RefreshToken이 유효하기 때문에
RefreshToken을 사용해 AccessToken을 발급받을 수 있음.
3. RefreshToken 만료 + AccessToken 유효
=> AccessToken이 만료될 때까지 AccessToken을 사용할 수 있음.
하지만 만료 기간이 짧기 때문에 이후 로그인이 필요함.
4. RefreshToken 만료 + AccessToken 만료
=> 로그인이 필요한 경우.
1번의 경우는 정상적으로 로그인이 유지되는 경우이고 3번과 4번의 경우는 로그인이 필요한 경우이다.
2번의 경우 RefreshToken을 사용해 AccessToken을 발급받는 과정이 필요한데 이 과정에서도 다양한 경우의 수가 있기 때문에 처리가 약간 까다롭다.
보안을 위해 AccessToken의 만료 기간은 짧게 가져간다.
예를 들어 AccessToken의 만료 기간이 10분이라면 10분 뒤에는 RefreshToken을 이용해 AccessToken을 발급 받아야 한다.
근데 이 RefreshToken으로 AccessToken을 발급받는 api를 호출하는 시점을 정하는 것이 쉽지 않다.
사용자가 서비스를 바로 사용할 수도 있지만 자리를 비우고 AccessToken 만료 시간 후에 돌아와 다시 하던 작업을 이어갈 수도 있기 때문이다.
따라서 AccessToken을 사용자 몰래 조용히 재발급 받기 위한 방법을 생각해 보았다.
말 그대로 api 요청 후 AccessToken이 만료되었다는 에러가 반환되면 AccessToken을 재발급 받는 요청을 보낸다.
그리고 새로 받아온 AccessToken을 사용해 다시 이전의 api 요청을 보내는 것이다.
이 경우 확실히 AccessToken이 만료되었을 때 재발급 요청을 보낼 수 있다.
하지만 이미 api 요청 시에도 다양한 이유로 에러가 발생할 수 있는데 "AccessToken 만료"라는 경우에 대해서 따로 에러 처리를 해야 하고 이 과정이 모든 api 요청에 들어가야 한다.
백엔드에서 AccessToken을 생성할 때 만료 기한을 정해준다.
만약 AccessToken의 만료 기간이 10분이라면 넉넉하게 AccessToken 발급 후 9분이 지났을 때 AccessToken을 재발급 받는다.
이 경우 따로 에러 처리를 할 필요 없기 때문에 편하다.
하지만 주기적으로 api 요청이 발생한다는 점과 유효 기간이 남았음에도 AccessToken을 발급받을 수 있다는 점에서 낭비가 발생한다.
이번 프로젝트는 2번, 주기적으로 토큰을 발급받는 방법을 선택했다.
api 요청에 대한 낭비가 일어난다는 단점이 있지만 모든 api 요청에 토큰 만료 시의 로직을 추가할 필요가 없다는 장점이 더 크게 다가왔기 때문이다.
구현을 시작하며 주기적으로 토큰을 발급받는 방법을 선택함에 따라 추가적으로 고민해야 할 부분이 있었다.
처음 구현 시에는 _app.tsx에서 setInterval
함수를 사용해 주기적으로 AccessToken을 발급받는 api를 호출하도록 했다.
// refresh token이 있을 경우 access token 주기적으로 재발급
useEffect(() => {
const timer = setInterval(() => getAccessToken(), SILENT_REFRESH_TIME);
return () => {
clearInterval(timer);
};
}, []);
하지만 새로고침을 하게되면 timer는 다시 처음부터 시작하기 때문에 실제 AccessToken의 만료 시점과 재발급 api 요청을 보내는 시간이 꼬일 수가 있다.
따라서 페이지가 리로드될 때마다 AccessToken을 새로 발급받고 그 이후부터는 timer에 의해 주기적으로 재발급하게 하면 위 문제를 해결할 수 있다.
// 페이지 리로드 시 AccessToken 발급
useEffect(() => getAccessToken(), [])
마지막으로 2번의 단점인 주기적으로 api 요청이 발생한다는 점을 보완하기 위해 setInterval 함수 안에 현재 브라우저가 포커스 되어 있는지의 여부를 판별해 포커스 되어 있을 때만 api 요청 함수를 호출하는 로직을 추가했다.
// refresh token이 있을 경우 access token 주기적으로 재발급
useEffect(() => {
const timer = setInterval(() => {
if (document.hasFocus()) getAccessToken();
}, SILENT_REFRESH_TIME);
return () => {
clearInterval(timer);
};
}, []);
이 방법을 이용하면 타이머는 돌아가지만 api 요청은 브라우저가 포커스 되어 있을 때만 보낼 수 있다.