fix: refreshToken active
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 1m45s
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 1m45s
This commit is contained in:
@@ -9,7 +9,7 @@ export const http = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true, // 쿠키(RefreshToken) 전송을 위해 필수
|
||||
withCredentials: true, // 쿠키 전송 허용 (필요 시)
|
||||
});
|
||||
|
||||
// 1. 요청 인터셉터: 헤더에 AccessToken 주입
|
||||
@@ -40,14 +40,14 @@ const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue = [];
|
||||
};
|
||||
|
||||
// 2. 응답 인터셉터: 401 또는 403 발생 시 토큰 갱신
|
||||
// 2. 응답 인터셉터: 401 발생 시 토큰 갱신 (RTR 적용)
|
||||
http.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
const status = error.response?.status; // 응답 상태 코드 확인
|
||||
const status = error.response?.status;
|
||||
|
||||
// 401(Unauthorized) 또는 403(Forbidden) 에러이고, 아직 재시도하지 않은 요청인 경우
|
||||
// 401(Unauthorized) 에러이고, 아직 재시도하지 않은 요청인 경우
|
||||
if ((status === 401 || status === 403) && !originalRequest._retry) {
|
||||
// 이미 갱신 중이라면 큐에 넣고 대기
|
||||
if (isRefreshing) {
|
||||
@@ -55,57 +55,65 @@ http.interceptors.response.use(
|
||||
failedQueue.push({ resolve, reject });
|
||||
})
|
||||
.then((token) => {
|
||||
// 대기하던 요청들도 새 토큰으로 헤더 교체 후 재시도
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers['Authorization'] = `Bearer ${token}`;
|
||||
originalRequest.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return http(originalRequest);
|
||||
})
|
||||
.catch((err) => Promise.reject(err));
|
||||
}
|
||||
|
||||
originalRequest._retry = true; // 재시도 플래그 설정 (무한 루프 방지)
|
||||
originalRequest._retry = true;
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const { refreshToken, login, logout } = useAuthStore.getState();
|
||||
const { accessToken, refreshToken, login, logout } = useAuthStore.getState();
|
||||
|
||||
// RefreshToken이 없으면 갱신 시도 없이 바로 로그아웃
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
// AccessToken이나 RefreshToken이 없으면 갱신 불가능 -> 로그아웃
|
||||
if (!accessToken || !refreshToken) {
|
||||
throw new Error('Tokens are missing for reissue');
|
||||
}
|
||||
|
||||
// 🛠️ 토큰 갱신 요청
|
||||
// 중요: refresh 요청도 쿠키/CORS 처리를 위해 withCredentials: true 추가
|
||||
// 🛠️ 토큰 갱신 요청 (Backend: POST /api/auth/reissue)
|
||||
// 백엔드 ReissueRequest 구조: { accessToken, refreshToken }
|
||||
const { data } = await axios.post(
|
||||
`${BASE_URL}/api/auth/refresh`,
|
||||
{ refreshToken },
|
||||
{ withCredentials: true }
|
||||
`${BASE_URL}/api/auth/reissue`,
|
||||
{
|
||||
accessToken,
|
||||
refreshToken
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true
|
||||
}
|
||||
);
|
||||
|
||||
// 새 토큰 저장
|
||||
// RTR(Refresh Token Rotation) 적용:
|
||||
// 백엔드에서 AccessToken 뿐만 아니라 새로운 RefreshToken도 줍니다.
|
||||
const newAccessToken = data.data.accessToken;
|
||||
const newRefreshToken = data.data.refreshToken || refreshToken;
|
||||
|
||||
login(newAccessToken, newRefreshToken); // 스토어 업데이트
|
||||
const newRefreshToken = data.data.refreshToken;
|
||||
|
||||
// 큐에 대기 중이던 요청들 처리 (새 토큰 전달)
|
||||
// Zustand 스토어에 새 토큰 쌍 업데이트
|
||||
login(newAccessToken, newRefreshToken);
|
||||
|
||||
// 큐에 대기 중이던 요청들 처리
|
||||
processQueue(null, newAccessToken);
|
||||
|
||||
// 실패했던 원래 요청 재시도 (헤더 안전하게 교체)
|
||||
// 실패했던 원래 요청 재시도
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
}
|
||||
return http(originalRequest);
|
||||
|
||||
} catch (refreshError) {
|
||||
// 갱신 실패 시 로그아웃 및 큐 정리
|
||||
// 갱신 실패 (RefreshToken 만료/위변조 등) -> 로그아웃 처리
|
||||
processQueue(refreshError, null);
|
||||
useAuthStore.getState().logout();
|
||||
|
||||
// 브라우저 환경에서만 로그인 페이지로 이동
|
||||
// 브라우저 환경인 경우 로그인 페이지로 리다이렉트
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
// alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
|
||||
@@ -6,8 +6,8 @@ import Sidebar from '@/components/layout/Sidebar';
|
||||
import TopHeader from '@/components/layout/TopHeader'; // 👈 import 추가
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dev Park Blog',
|
||||
description: '개발 기록 블로그',
|
||||
title: 'WYPark Blog',
|
||||
description: '개발 블로그',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
Reference in New Issue
Block a user