백서버에서 온 accessToken과 refreshToken을 브라우저 cookie에 담기 위해 react-cookies라이브러리를 사용했다.
/utils/setToken.ts
import reactCookies from 'react-cookies';
let savedRefreshTokenExpireTime = [];
const onlyHTTPS = process.env.NODE_ENV === 'development' ? false : true;
function setToken(
accessToken: string,
refreshToken: string,
omsID: string,
accessTokenExpireTime: string,
refreshTokenExpireTime?: string,
) {
if (refreshTokenExpireTime) {
savedRefreshTokenExpireTime.push(new Date(refreshTokenExpireTime));
}
reactCookies.save('accessToken', accessToken, {
path: '/',
expires: new Date(accessTokenExpireTime),
secure: onlyHTTPS,
});
reactCookies.save('refreshToken', refreshToken, {
path: '/',
expires: savedRefreshTokenExpireTime[0],
secure: onlyHTTPS,
});
}
export default setToken;
/redux/reducers/user.ts
.addCase(PostSignIn.fulfilled, (state, action) => {
state.PostSignInLoading = false;
state.me = action.payload;
state.PostSignInDone = true;
const accessToken = action.payload.accessToken;
const refreshToken = action.payload.refreshToken;
const accessTokenExpireTime = action.payload.accessTokenExpireTime;
const refreshTokenExpireTime = action.payload.refreshTokenExpireTime;
setToken(accessToken, refreshToken,accessTokenExpireTime, refreshTokenExpireTime);
})
/redux/actions/index.ts
import reactCookies from 'react-cookies';
import axios from 'axios';
import setToken from '@utils/setToken';
import { message } from 'antd';
import router from 'next/router';
const baseURL = process.env.NODE_ENV === 'development' ? '/' : 'https://example.co.kr/';
export const axiosApiInstance = axios.create({
baseURL: baseURL,
withCredentials: true,
});
export const axiosApiRefreshToken = axios.create({
baseURL: baseURL,
withCredentials: true,
});
axiosApiInstance.interceptors.request.use(
async (config) => {
const accessTokenByCookies = await Promise.resolve(reactCookies.load('accessToken'));
config.headers = {
Authorization: `Bearer ${accessTokenByCookies}`,
Accept: 'application/json',
};
return config;
},
(error) => {
Promise.reject(error);
},
);
axiosApiRefreshToken.interceptors.request.use(
async (config) => {
const refreshTokenByCookies = await Promise.resolve(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'));
if (error.response?.status === 401 && originalRequest.url === '/Web/RefreshToken') {
console.log('Prevent infinite loops');
return Promise.reject(error);
}
if (error.response?.status === 401 && refreshTokenByCookies) {
try {
if (refreshTokenByCookies) {
const response = await axiosApiRefreshToken.get('/Web/RefreshToken');
const newAccessToken = response.data.accessToken;
setToken(newAccessToken, refreshTokenByCookies, response.data.accessTokenExpireTime);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axios(originalRequest);
} else {
// console.log('refreshToken 만료1 - access갱신 후에도 에러');
reactCookies.remove('accessToken', { path: '/' });
reactCookies.remove('refreshToken', { path: '/' });
router.replace('/');
message.warn('세션이 만료되어 다시 로그인이 필요합니다.', 4);
}
} catch (error) {
// console.log('refreshToken 만료2 - 다른 기기 로그인');
reactCookies.remove('accessToken', { path: '/' });
reactCookies.remove('refreshToken', { path: '/' });
router.replace('/');
message.warn('다른 기기에서 로그인되었습니다. 다시 로그인해주세요.', 4);
}
}
return Promise.reject(error);
},
);
/redux/actions/user.ts
import { axiosApiInstance } from './index';
export const GetMyInfo = createAsyncThunk<
GetMyInfoResponse,
GetMyInfoRequest,
{
dispatch: AppDispatch;
}
>('Web/GetMyInfo', async (data, { rejectWithValue }) => {
try {
const response = await axiosApiInstance.get(`/Web/GetMyInfo`);
return response.data as GetMyInfoResponse;
} catch (error) {
return rejectWithValue(error.statusCode);
}
});
/pages/order.tsx
import wrapper from '@redux/store/configureStore';
import { GetServerSideProps } from 'next';
import nextCookies from 'next-cookies';
import axios from 'axios';
import orderSlice from '@redux/reducers/order';
export const getServerSideProps: GetServerSideProps = wrapper.getServerSideProps(async (context) => {
const allCookies = nextCookies(context);
const accessTokenByCookies = allCookies['accessToken'];
const refreshTokenByCookies = allCookies['refreshToken'];
const baseURL = 'https://oms.modument.co.kr/';
axios.defaults.baseURL = baseURL;
axios.defaults.headers.Authorization = `Bearer ${accessTokenByCookies}`;
await context.store.dispatch(orderSlice.actions.redirectDuringSSR(true));
if (accessTokenByCookies) {
await context.store.dispatch<any>(
GetOrderLists({
...
}),
);
} else {
try {
axios.defaults.headers.Authorization = `Bearer ${refreshTokenByCookies}`;
const response = await axios.get('/Web/RefreshToken');
const newAccessToken = response.data.accessToken;
axios.defaults.headers.Authorization = `Bearer ${newAccessToken}`;
await context.store.dispatch<any>(
GetOrderLists({
...
}),
);
} catch (error) {
return {
redirect: {
destination: '/',
statusCode: 301,
},
};
}
}
return {
props: {},
};
});