Test: 리프레시 토큰 쿠키

This commit is contained in:
정현조 2024-09-11 15:00:29 +09:00
parent 81187fb0f2
commit 8efc4c1494
3 changed files with 98 additions and 59 deletions

View File

@ -1,20 +1,8 @@
import api from '@/api/axiosConfig'; import api from '@/api/axiosConfig';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
export const reissueTokenApi = async () => { export const reissueTokenApi = () => {
try { return api.post('/api/auth/reissue', null, { withCredentials: true }).then((response) => response.data);
const response = await api.post('/api/auth/reissue', null, {
withCredentials: true,
});
return response.data;
} catch (error) {
if (error instanceof AxiosError) {
console.error('토큰 재발급 실패:', error.response?.data?.message || '알 수 없는 오류');
} else {
console.error('알 수 없는 오류가 발생했습니다.');
}
throw error;
}
}; };
export const fetchProfileApi = async () => { export const fetchProfileApi = async () => {

View File

@ -1,4 +1,4 @@
import axios from 'axios'; import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
const baseURL = 'https://j11s002.p.ssafy.io'; const baseURL = 'https://j11s002.p.ssafy.io';
@ -8,56 +8,83 @@ const api = axios.create({
withCredentials: true, withCredentials: true,
}); });
api.interceptors.request.use((config) => { let isTokenRefreshing = false;
type FailedRequest = {
resolve: (value?: string | undefined) => void;
reject: (reason?: unknown) => void;
};
let failedQueue: FailedRequest[] = [];
const processQueue = (error: Error | null, token: string | undefined = undefined): void => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
if (token) { if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
return config; return config;
}); });
api.interceptors.response.use( api.interceptors.response.use(
(response) => response, (response: AxiosResponse) => response,
async (error) => { async (error: AxiosError) => {
const originalRequest = error.config; const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
if (error.response?.status === 401 && originalRequest._retry) {
console.error('토큰 재발급 중 401 오류가 발생했습니다. 로그아웃 처리합니다.');
alert('세션이 만료되었습니다. 다시 로그인해 주세요.');
useAuthStore.getState().clearAuth();
window.location.href = '/';
return Promise.reject(error);
}
if (error.response?.status === 401 && !originalRequest._retry) { if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; if (isTokenRefreshing) {
return new Promise<string | undefined>((resolve, reject) => {
try { failedQueue.push({ resolve, reject });
const response = await api.post('/api/auth/reissue', null, { })
withCredentials: true, .then((token) => {
}); if (token && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
if (!response.data?.data?.accessToken) { }
alert('잘못된 토큰 재발급 응답입니다. 다시 로그인해 주세요.'); return api(originalRequest);
useAuthStore.getState().clearAuth(); })
window.location.href = '/'; .catch((err) => Promise.reject(err));
return Promise.reject(new Error('Invalid token reissue response'));
} }
const newAccessToken = response.data.data.accessToken; originalRequest._retry = true;
isTokenRefreshing = true;
return api
.post('/api/auth/reissue', null, { withCredentials: true })
.then((response) => {
const newAccessToken = response.data?.data?.accessToken;
if (!newAccessToken) {
throw new Error('Invalid token reissue response');
}
useAuthStore.getState().setLoggedIn(true, newAccessToken); useAuthStore.getState().setLoggedIn(true, newAccessToken);
localStorage.setItem('accessToken', newAccessToken); localStorage.setItem('accessToken', newAccessToken);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; processQueue(null, newAccessToken);
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
}
return api(originalRequest); return api(originalRequest);
} catch (reissueError) { })
.catch((reissueError: Error) => {
processQueue(reissueError, undefined);
console.error('토큰 재발급 실패:', reissueError); console.error('토큰 재발급 실패:', reissueError);
alert('토큰 재발급에 실패했습니다. 다시 로그인해 주세요.');
useAuthStore.getState().clearAuth(); useAuthStore.getState().clearAuth();
window.location.href = '/'; window.location.href = '/';
return Promise.reject(reissueError); return Promise.reject(reissueError);
} })
.finally(() => {
isTokenRefreshing = false;
});
} }
if (error.response?.status === 400) { if (error.response?.status === 400) {

View File

@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png'; import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { fetchProfileApi } from '@/api/authApi'; import { fetchProfileApi, reissueTokenApi } from '@/api/authApi';
const DOMAIN = 'https://j11s002.p.ssafy.io'; const DOMAIN = 'https://j11s002.p.ssafy.io';
@ -42,6 +42,19 @@ export default function Home() {
navigate('/browse'); navigate('/browse');
}; };
const handleReissueToken = async () => {
try {
const response = await reissueTokenApi();
console.log('토큰 재발급 성공:', response);
alert('토큰 재발급 성공! 새로운 액세스 토큰을 콘솔에서 확인하세요.');
} catch (error) {
console.error('토큰 재발급 실패:', error);
alert('토큰 재발급에 실패했습니다. 다시 시도해 주세요.');
}
};
const isHidden = true;
return ( return (
<div className="flex h-full flex-col items-center justify-center bg-gray-50 p-8"> <div className="flex h-full flex-col items-center justify-center bg-gray-50 p-8">
<div className="mb-6 max-w-xl rounded-lg bg-white p-6 shadow-lg"> <div className="mb-6 max-w-xl rounded-lg bg-white p-6 shadow-lg">
@ -75,6 +88,7 @@ export default function Home() {
/> />
</button> </button>
) : ( ) : (
<>
<Button <Button
variant="outlinePrimary" variant="outlinePrimary"
size="lg" size="lg"
@ -82,6 +96,16 @@ export default function Home() {
> >
</Button> </Button>
<Button
variant="outlinePrimary"
size="lg"
onClick={handleReissueToken}
className="mt-4"
style={{ display: isHidden ? 'none' : 'block' }}
>
</Button>
</>
)} )}
</div> </div>
); );