🧐 처음 생각한 인증 과정
현재 로그인이 된 유저라면 모든 페이지에서 처음에 /members 요청으로 유저가 인증된 유저인지 판단합니다.
만약 Access Token이 만료되어서 /members 요청에서 error status가 410이 온다면, /reissue 요청으로 Refresh Token을 사용하여 Access Token을 재발급합니다. 재발급이 되었다면 다시 /members 요청을 보내어 인증된 유저인지 다시 확인합니다.
만약 RefreshToken마저 만료되었다면 로그인을 해제합니다.
😡 문제 상황
- Access Token이 만료된 상황에서 다른 페이지로 이동하면 reissue는 작동하지만, members 요청은 보내지 않아서 인증된 유저라고 판단하지 않게 되어 유저의 데이터를 불러오지 않는 에러가 생겼습니다.
- Refresh Token까지 만료되었을 때, reissue는 410 에러를 뱉습니다. 그러면 로그인이 해제되고 끝나야 하는데, members 요청을 보내고 또 reissue 요청을 보내고, 이게 계속 반복되는 에러가 생겼습니다.
🔍 에러가 발생한 코드
instance.interceptors.response.use(
(response) => {
return response;
},
async (error: AxiosError) => {
if (error && error.response?.status === 410) {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
return axios
.post(
'/api/auth/reissue',
{},
{
baseURL: PROXY,
headers: {
'Content-Type': 'application/json',
Authorization: accessToken,
RefreshToken: refreshToken,
},
}
)
.then((response) => {
if (response && response.headers) {
const newAccessToken = response.headers.authorization as string;
localStorage.setItem('accessToken', newAccessToken ?? '');
localStorage.setItem('isLogin', JSON.stringify(true));
}
})
.catch((err: AxiosError) => {
throw err;
}
return Promise.reject(error);
}
);
저희는 axios에서 사용자 정의 instance를 만들어서 사용했고, interceptor를 활용하여 재사용성과 유지보수성을 높였습니다.
인증된 유저인지 확인 요청을 보낼 때 토큰을 헤더에 넣어서 보내야 하고, 응답을 받았을 때 토큰이 만료되었는지 판단하고 재요청을 보내야 했기 때문입니다.
위 코드는 응답 인터셉터를 설정한 코드입니다.
발생한 에러는 reissue까지는 잘 보내는데, members를 재요청 보내지 않는다는 것이었습니다.
.then((response) => {
if (response && response.headers) {
const newAccessToken = response.headers.authorization as string;
localStorage.setItem('accessToken', newAccessToken ?? '');
localStorage.setItem('isLogin', JSON.stringify(true));
}
})
그렇다면 이 부분에서 발생한 에러라고 추측했습니다.
로컬스토리지에만 새로 발급된 토큰을 저장하면 알아서 판단해서 바꿔주겠지? 라고 생각하고 짰던 코드 같은데, 다시 보니 요청을 보내는 코드가 없었으니 당연히 재요청을 보내지 않았던 것이었습니다.
😄 해결
async (error: AxiosError) => {
const originalConfig = error.config;
const status = error.response?.status;
if (status === 410) {
try {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios({
url: `${PROXY}/api/auth/reissue`,
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken,
RefreshToken: refreshToken,
},
});
...
}
}
에러가 발생했다면 발생한 에러의 config와 status를 저장해 놓습니다.
status가 410이라면 로컬스토리지에서 Access Token과 Refresh Token을 가져와서 헤더에 담아주고 reissue 요청을 보냅니다.
if (response) {
const newAccessToken = response.headers.authorization as string;
const newRefreshToken = response.headers.refreshtoken as string;
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
localStorage.setItem('isLogin', JSON.stringify(true));
if (originalConfig && originalConfig.headers) {
originalConfig.headers = {
...originalConfig.headers,
Authorization: newAccessToken,
Refreshtoken: newRefreshToken,
} as unknown as AxiosRequestHeaders;
return await instance(originalConfig);
}
}
reissue 요청을 보내서 받은 response가 있다면, response의 헤더에서 Access Token과 Refresh Token을 가져와서 로컬스토리지에 저장합니다.
그리고 저장해 놓았던 originalConfig의 헤더에 새로 발급받은 토큰들을 담아주고 instance에 config를 담아 요청합니다.
catch (err) {
localStorage.clear();
window.location.reload();
throw err;
}
catch 블록에서 로컬스토리지에 저장된 토큰과 로그인 정보를 날려주고, 새로고침을 한번 해줬습니다.
이렇게 해서 Refresh Token까지 만료되었을 때 더 이상 요청을 보내지 않고 로그인이 해제됩니다.
🗒️ 에러를 해결한 전체 코드
instance.interceptors.response.use(
(response) => {
return response;
},
async (error: AxiosError) => {
const originalConfig = error.config;
const status = error.response?.status;
if (status === 410) {
try {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios({
url: `${PROXY}/api/auth/reissue`,
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken,
RefreshToken: refreshToken,
},
});
if (response) {
const newAccessToken = response.headers.authorization as string;
const newRefreshToken = response.headers.refreshtoken as string;
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
localStorage.setItem('isLogin', JSON.stringify(true));
if (originalConfig && originalConfig.headers) {
originalConfig.headers = {
...originalConfig.headers,
Authorization: newAccessToken,
Refreshtoken: newRefreshToken,
} as unknown as AxiosRequestHeaders;
return await instance(originalConfig);
}
}
} catch (err) {
localStorage.clear();
window.location.reload();
throw err;
}
}
return Promise.reject(error);
}
);
😇 마무리
잘못된 정보가 있는 것 같다면 댓글로 피드백 부탁드립니다!!
'Project' 카테고리의 다른 글
[하루메이트] 리팩토링 - 초기 로딩 속도 줄이기 ver.1 (0) | 2023.08.04 |
---|---|
[하루메이트] 프로젝트 회고 (0) | 2023.07.26 |
[하루메이트] 일정 수정 기능 중 useQuery 타입 에러 해결, 일정 30개 제한 기능 (0) | 2023.07.17 |
[하루메이트] 3일동안 한거라곤 반응형과 버그 잡기 - 22.07.13~15 (0) | 2023.07.15 |
[하루메이트] 오늘의 회고 - 22.07.12 (0) | 2023.07.12 |