프론트단에서 JWT 보안상 안정성을 강화하기위해 accessToken은 redux에, refreshToken은 cookies에 보관하는 방식으로 axios.interceptor를 수정하는 과정에서 문제가 발생했다. 아래 코드와 같이
axios intercepter 안에서 dispatch를 가져와 사용하려고 하니 정상적으로 작동하지 않았다.
import { useAppDispatch } from '@redux/store/configureStore';
axiosApiInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const dispatch = useAppDispatch();//이 코드 이후부터 코드들이 작동하지 않았다.
}
Hooks, such as useDispatch(), can only be used in the body of a function component. You cannot use useDispatch in an interceptor. If you need to dispatch an action in an interceptor, you will need to have a reference to the store object, and call store.dispatch(/ action /)
https://stackoverflow.com/questions/60643890/redux-dispatch-not-work-when-use-in-axios-interceptors
useDispatch hook은 함수 component 안에서만 사용할 수 있다. interceptor 안에서는 사용할 수 없어서 에러가 난 것이다.
사용해본 결과 다음과 같이 configureStore에서 바로 가져온 store 객체에 있는 dispatch는 작동이 안된다.
import { store } from '@redux/store/configureStore';
axiosApiInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
store.dispatch(userSlice.actions.setAccessToken(newAccessToken));
},
);
export const PostConfirmUserInfo = createAsyncThunk<PostUpdateMyInfoReponse, GetMyInfoRequest>(
'Web/ConfirmUserInfo',
async (data, { getState, rejectWithValue, dispatch })/ => {
const response = await setupAxios(getState(), dispatch).post(`/Web/ConfirmUserInfo`, data);/dispatch매개변수를 가져와 setupAxios 함수의 인수로 넣어서 사용한다.
return response.data as PostUpdateMyInfoReponse;
},
);
전체코드는 다음과 같다.
import reactCookies from 'react-cookies';
import axios from 'axios';
import setToken from '@utils/setToken';
import { userSlice } from '../reducers/user';
import { message } from 'antd';
import router from 'next/router';
function setupAxios(state) {
const baseURL = process.env.NODE_ENV === 'development' ? '/' : 'https://example.co.kr/'; //개발모드일 때 proxy서버로 요청보내고 배포모드일 때 실서버로 요청보내기
const axiosApiInstance = axios.create({
baseURL: baseURL,
withCredentials: true,
});
const axiosApiRefreshToken = axios.create({
baseURL: baseURL,
withCredentials: true,
});
axiosApiInstance.interceptors.request.use(
(config) => {
const accessToken = state.user.accessToken;
// console.log('accessToken', accessToken);
config.headers = {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
};
return config;
},
(error) => {
Promise.reject(error);
},
);
axiosApiRefreshToken.interceptors.request.use(
(config) => {
const refreshTokenByCookies = reactCookies.load('refreshToken');
config.headers = {
Authorization: `Bearer ${refreshTokenByCookies}`,
Accept: 'application/json',
};
return config;
},
(error) => {
Promise.reject(error);
},
);
axiosApiInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
const refreshTokenByCookies = await Promise.resolve(reactCookies.load('refreshToken'));
console.log('originalRequest.url', originalRequest.url);
if (
(error.response?.status === 401 || error.response?.status === 403) &&
originalRequest.url === '/Web/RefreshToken'
) {
console.log('Prevent infinite loops');
dispatch(userSlice.actions.signOut());
message.warn('만료된 토큰으로 반복해서 요청이 가고 있습니다. 다시 로그인이 필요합니다.', 4);
return Promise.reject(error);
}
// console.log('error.response?.status', error.response?.status);
// console.log('refreshTokenByCookies', refreshTokenByCookies);
if (error.response?.status === 401 || error.response?.status === 403) {
if (refreshTokenByCookies) {
try {
const response = await axiosApiRefreshToken.get('/Web/RefreshToken');
const newAccessToken = response.data.accessToken;
dispatch(userSlice.actions.setAccessToken(newAccessToken));
setToken(refreshTokenByCookies);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axios(originalRequest);
} catch (error) {
console.log('error', error);
message.warn('자동로그인 가능 기간이 지났습니다. 다시 로그인이 필요합니다.', 4);
dispatch(userSlice.actions.signOut());
}
}
}
return Promise.reject(error);
},
);
return axiosApiInstance;
}
export default setupAxios;
참고로 rtk-query의 경우 BaseQueryFn의 매개변수 중 api에 담긴 dispatch를 사용할 수 있다.
const baseQueryWithIntercept: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError | ResponseErrorType> =
async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
if (result.error && (result.error.status === 401 || result.error.status === 403)) {
const refreshTokenByCookies = reactCookies.load('refreshToken');
const refreshTokenResult: QueryReturnValue<any, FetchBaseQueryError | ResponseErrorType, FetchBaseQueryMeta> =
await baseQuery(
{
url: 'Web/RefreshToken/',
headers: {
Authorization: `Bearer ${refreshTokenByCookies}`,
},
},
api,
extraOptions,
);
console.log('refreshTokenResult', refreshTokenResult);
if (!!refreshTokenResult.data) {
const newAccessToken = refreshTokenResult.data.accessToken;
api.dispatch(userSlice.actions.setAccessToken(newAccessToken));
setToken(refreshTokenByCookies);
result = await baseQuery(args, api, extraOptions);
} else if (refreshTokenResult?.error?.status === 401) {
message.error('내 정보 가져오기에 실패했습니다. 다시 로그인이 필요합니다.', 4);
api.dispatch(userSlice.actions.signOut());
}
return result;
};
export const rtkApi = createApi({
baseQuery: baseQueryWithIntercept,
reducerPath: 'rtkApi',
tagTypes: ['User'],
endpoints: (build) => ({
...
})
)
})
다른 기기 로그인 후 refreshToken 만료 시 setAxios의 try catch문의 catch부분이 실행되고 있다.
catch (error) {
console.log('error', error);
message.warn('자동로그인 가능 기간이 지났습니다. 다시 로그인이 필요합니다.', 4);
dispatch(userSlice.actions.signOut());
}
rtk-query의 경우 아래 코드 부분이 실행되며 로그아웃이 되고 있다.
else if (refreshTokenResult?.error?.status === 401) {
message.error('내 정보 가져오기에 실패했습니다. 다시 로그인이 필요합니다.', 4);
api.dispatch(userSlice.actions.signOut());
}
rtk-query의 refreshTokenResult에는 크게 data와 meta가 있고 그 안에는 다음과 같은 정보들이 들어있다.
출처:
https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0
https://redux-toolkit.js.org/api/createAsyncThunk
https://stackoverflow.com/questions/52946376/reactjs-axios-interceptors-how-dispatch-a-logout-action