프로젝트를 진행하면서 인증 처리를 하려면 JWT 방식인 AccessToken과 RefreshToken을 이용해 다음과 같이 처리 해주어야 했습니다.
지금껏, 명확한 방법을 찾지 못한 채 생각 나는 대로만 로직을 작성했기 때문에 제대로 된 검증 및 동작 테스트를 하지 못했습니다. 코드도 굉장히 복잡한 상태라, 깔끔하고 명확한 토큰 인증 방법을 갈망했습니다 🤔
현재는 토큰들을 쿠키에 저장하는데, 개선 전에는 쿠키를 받아와서 매번 페이지가 마운트 될 때 마다 axios instance common
헤더에 삽입해주고, 유실되면 다시 삽입해주고... 번거로운 작업을 했었습니다...
좀 더 나은 방법을 구글링 하던 중, axios interceptor 를 사용하여 http 요청 및 응답을 가로채어 필요한 기능을 추가할 수 있다는 정보를 얻었습니다!! 무야홍.
Axios Interceptor는 axios의 request와 response를 가로채어 필요한 기능을 추가할 수 있습니다.
axios의 return type이 Promise인 점을 이용해 특정 요청 전 부가 작업을 할 수 있게 해주는 라이브러리 입니다.
http request가 서버에 전달되기 전에 호출됩니다. 요청 헤더 및 인증, 로깅 등을 위해 사용합니다.
서버에 보낸 요청에 대한 response를 받은 후 then
또는 catch
로 전달되기 전에 호출 됩니다.
이전까진...
axios
요청보낼 때 마다 config headers
에 access_token
을 삽입하거나,axios instance common header
에 access_token
을 삽입했었는데, interceptor를 사용하면 정말 쉽게 해결됩니다!
const authAxios: AxiosInstance = axios.create({
baseURL:
process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_BACKEND : process.env.NEXT_PUBLIC_DEPLOY_BACKEND,
timeout: 100000000,
headers: { 'Content-Type': 'application/json' },
});
우선, axios instance
를 생성해줍니다.
/*
http가 request를 보내기 전에 호출되는 함수이다.
cookie에 저장된 access_token을 인증 헤더에 삽입하여 요청마다 보내준다.
*/
const onRequest = (config: AxiosRequestConfig): AxiosRequestConfig => {
const access_token = getCookie('access_token');
/* 토큰이 있을 경우 헤더에 삽입한다. 없을 경우 빈 문자열을 넣는다(null은 안됨) */
config.headers = {
Authorization: !!access_token ? `Bearer ${access_token}` : '',
};
return config;
};
const onErrorRequest = (err: AxiosError | Error): Promise<AxiosError> => {
return Promise.reject(err);
};
authAxios.interceptors.request.use(onRequest, onErrorRequest);
getCookie
함수는 key값을 전달하면 해당하는 value를 쿠키에서 찾아 반환하는 함수입니다.access_token
을 불러옵니다.access_token
이 존재한다면 헤더에 삽입, 아니라면 빈 문자열을 삽입합니다.이렇게 되면, 쿠키에 저장된 토큰값으로 매 요청마다 헤더에 토큰이 들어갑니다!
토큰 갱신을 위해 발급받은 RefreshToken으로 새로운 AccessToken을 발급 받는 과정은 다음과 같습니다.
- 로그인에 성공하면 서버는 클라이언트에게 AccessToken과 RefreshToken을 발급한다.
- 인증이 필요한 요청 헤더에 AccessToken을 담아서 요청한다.(위의 2번 참고)
- 이 때, 클라이언트가 가진 토큰이 만료되었다면, 요청을 보냈을 때 서버에서는 토큰 만료
401
코드와 함께 요청을 거부합니다.- 클라이언트는 토큰 갱신을 위해 RefreshToken을 서버
Refresh Api
에 보냅니다.- 서버는 RefreshToken의 유효성을 검증하고 유효한다면 새 AccessToken을 발급한다.
- 클라이언트는 새로 받은 AccessToken으로 교체하고 기존 요청을 다시 보낸다.
/*
http가 response를 보내기 전에 가로채어 응답값을 검증한다.
access_token이 만료되었을 때 cookie에 저장된 refresh_token를 검증하여 access_token을 재요청한다.
*/
/* http response가 then으로 넘어가기 전에 호출되는 함수이다. */
const onResponse = (res: AxiosResponse): AxiosResponse => {
return res;
};
/* http response가 catch로 넘어가기 전에 호출되는 함수이다.*/
const onErrorResponse = async (err: AxiosError | Error): Promise<AxiosError> => {
const _err = err as unknown as AxiosError; // err 객체의 타입은 unknown이므로 타입 단언을 해주어야 한다
const { response } = _err; // err 객체에서 response 를 구조 분해 할당
const originalConfig = _err?.config; // 기존의 요청 정보를 저장한다.
if (response && response.status === 403) {
const access_token = getCookie('access_token'); // 현재 만료된 access token;
const refresh_token = getCookie('refresh_token'); // 리프레시 토큰이 있을 경우 가져온다.
if (!!refresh_token === false) {
// refresh token이 쿠키에서 삭제 또는 만료 되었을 경우
console.log('리프레시 토큰 쿠키 삭제 또는 만료됨 ');
// 만료 처리
} else {
try {
// 만료된 access token과 refresh token을 이용해 리프레시api에 갱신 요청
const data = await notAuthAxios.put(
`/refresh`,
{}, // 백엔드에서 빈 객체 body를 받을 수 있도록 수정 요청
{ headers: { Refresh: `Bearer ${refresh_token}`, Authorization: `Bearer ${access_token}` } },
);
if (data) {
// 응답값이 있을 경우 새로 발급 받은 토큰을 저장한다.
await saveToken(accessToken); // 토큰을 쿠키에 저장 비동기 함수
return await authAxios.request(originalConfig);
}
} catch (err) {
// 리프레시 토큰 만료. 로그아웃 처리
const _err = err as unknown as AxiosError;
console.log(_err?.config?.data);
}
}
}
return Promise.reject(err);
};
authAxios.interceptors.response.use(onResponse, onErrorResponse);
saveToken
함수는 AccessToken을 쿠키에 저장하는 함수 입니다.주로, AccessToken은 주기를 짧게 갖고, RefreshToken은 주기를 길게 하여 Token 변조를 방지합니다. AccessToken 주기가 짧은 만큼 토큰 만료 요청 거부도 잦게 됩니다. 이렇게 되면, 사용자 경험 측면에서 잦은 재로그인이 번거로울 수 있습니다. 그래서 토큰 갱신을 자연스럽게 한 후 다시 서버에 요청을 보내어 자연스러운 로그인 연장 효과를 볼 수 있습니다. 즉, 사용자가 체감하는 로그인 유지 시간은 RefreshToken 유효 시간과 동일하게 됩니다.
axios interceptor를 타입스크립트와 함께 사용하여 조금 더 안정된 토큰 검증 및 갱신을 구현할 수 있었습니다. 흩어져 있던 토큰 관리 코드가 한 곳에 모이며 불필요한 코드가 제거되었습니다. 매우 만족하는 상황!
다만, 아직 토큰을 쿠키에 저장하고 있기 때문에 CSRF 공격 및 XSS 공격 대응이 어려운 상황입니다 ㅜㅜㅜㅜ httpOnly
나 sameSite
옵션을 이용해 쿠키를 백엔드로 부터 받아서 저장하는 방향으로 보안을 강화하는 방안으로 개선할 수 있습니다. CSRF 공격 방어를 위해서는 조금 더 공부가 필요할 것 같습니다!
JWT 저장 및 보안 이슈에 대해서는 아래 링크를 참고하였습니다.
https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie
공부하며 작성한 글이기 때문에 부족한 부분이나 궁금한 점이 있으시다면 댓글 자유롭게 남겨주세요 😊 감사합니다! 코드는 계속 개선되고 있습니다!
출처
- https://velog.io/@jin0106/axios-interceptor%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%86%A0%ED%81%B0-%EC%B2%98%EB%A6%AC
- https://brunch.co.kr/@14e1a0684a6c4d5/6#:~:text=%EC%A0%95%EB%A6%AC%ED%95%98%EC%9E%90%EB%A9%B4%2C%20axios%20interceptor%EB%8A%94,%EC%88%98%20%EC%9E%88%EA%B2%8C%20%ED%95%B4%EC%A3%BC%EB%8A%94%20library%EC%9E%85%EB%8B%88%EB%8B%A4.
- https://velog.io/@apro_xo/axios-axios-interceptors%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-JWT-RefreshToken-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0