access_token 이 없지만 refresh_token 이 있으면 access_token 갱신처리
이미 로그인 한 상태에서 로그인 페이지로 이동하면 첫페이지로 리다이렉트
This commit is contained in:
43
src/App.tsx
43
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import React, { Suspense, useEffect } from 'react'
|
import React, { Suspense, useEffect } from 'react'
|
||||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
import { HashRouter, Route, Routes, Navigate } from 'react-router-dom'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { RootState } from 'src/store'
|
import { RootState } from 'src/store'
|
||||||
|
|
||||||
@@ -19,9 +19,13 @@ const Page404 = React.lazy(() => import('src/views/pages/page404/Page404'))
|
|||||||
const Page500 = React.lazy(() => import('src/views/pages/page500/Page500'))
|
const Page500 = React.lazy(() => import('src/views/pages/page500/Page500'))
|
||||||
const ProtectedRoute = React.lazy(() => import('src/routes/ProtectedRoute'))
|
const ProtectedRoute = React.lazy(() => import('src/routes/ProtectedRoute'))
|
||||||
|
|
||||||
|
import { useAuth } from 'src/hooks/useAuth'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
|
const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
|
||||||
const storedTheme = useSelector((state: RootState) => state.theme)
|
const storedTheme = useSelector((state: RootState) => state.theme)
|
||||||
|
const { state: authState } = useAuth()
|
||||||
|
const { isAuthenticated, loading: authLoading } = authState
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.href.split('?')[1])
|
const urlParams = new URLSearchParams(window.location.href.split('?')[1])
|
||||||
@@ -37,6 +41,14 @@ const App = () => {
|
|||||||
setColorMode(storedTheme)
|
setColorMode(storedTheme)
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (authLoading) {
|
||||||
|
return (
|
||||||
|
<div className="pt-3 text-center">
|
||||||
|
<CSpinner color="primary" variant="grow" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Suspense
|
<Suspense
|
||||||
@@ -47,25 +59,20 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
{/* 1. 로그인 여부와 관계없이 항상 독립적으로 표시되는 페이지 */}
|
||||||
<Route
|
<Route path="/register" element={<Register />} />
|
||||||
path="/register"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Register />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/404" element={<Page404 />} />
|
<Route path="/404" element={<Page404 />} />
|
||||||
<Route path="/500" element={<Page500 />} />
|
<Route path="/500" element={<Page500 />} />
|
||||||
<Route
|
|
||||||
path="*"
|
{/* 2. 인증 상태에 따른 조건부 라우팅 */}
|
||||||
element={
|
{!isAuthenticated ? (
|
||||||
<ProtectedRoute>
|
<>
|
||||||
<DefaultLayout />
|
<Route path="/login" element={<Login />} />
|
||||||
</ProtectedRoute>
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
}
|
</>
|
||||||
/>
|
) : (
|
||||||
|
<Route path="*" element={<DefaultLayout />} />
|
||||||
|
)}
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ export const getAccessTokenFromCookie = (): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRefreshTokenFromCookie = (): string | null => {
|
||||||
|
const name = 'refresh_token';
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export interface LoginCredentials {
|
export interface LoginCredentials {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -77,3 +85,14 @@ export const getUserFromToken = (token: string): DecodedToken | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Access Token 갱신 API
|
||||||
|
export const renewAccessToken = async (refreshToken: string): Promise<AuthResponse> => {
|
||||||
|
// Refresh Token을 Header에 담아 전송 (Authorization: Bearer <token>)
|
||||||
|
const response = await axios.post<AuthResponse>('/auth/renewAccessToken', null, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${refreshToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const API_URL = '/api/v1/';
|
|||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: API_URL,
|
baseURL: API_URL,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ const AppContent = () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Route path="/" element={<Navigate to="dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
<Route path="/login" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</CContainer>
|
</CContainer>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useReducer, useEffect } from 'react';
|
import React, { createContext, useReducer, useEffect } from 'react';
|
||||||
|
|
||||||
import { isTokenValid, getAccessTokenFromCookie, getUserFromToken } from 'src/axios/authService';
|
import { isTokenValid, getAccessTokenFromCookie, getUserFromToken, getRefreshTokenFromCookie, renewAccessToken } from 'src/axios/authService';
|
||||||
|
|
||||||
// 사용자 타입 정의
|
// 사용자 타입 정의
|
||||||
export interface Member {
|
export interface Member {
|
||||||
@@ -122,7 +122,28 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadUser = async () => {
|
const loadUser = async () => {
|
||||||
// localStorage 또는 Cookie에서 토큰 확인
|
// localStorage 또는 Cookie에서 토큰 확인
|
||||||
let token = localStorage.getItem('accessToken') || getAccessTokenFromCookie();
|
let token = localStorage.getItem('accessToken') || localStorage.getItem('access_token') || getAccessTokenFromCookie();
|
||||||
|
|
||||||
|
if (!token || !isTokenValid(token)) {
|
||||||
|
// Access Token이 없거나 만료된 경우 Refresh Token 확인
|
||||||
|
let refreshToken = localStorage.getItem('refreshToken') || localStorage.getItem('refresh_token') || getRefreshTokenFromCookie();
|
||||||
|
|
||||||
|
// refreshToken으로 Access Token 갱신 시도
|
||||||
|
try {
|
||||||
|
const response = await renewAccessToken(refreshToken || '');
|
||||||
|
if (response.resultCode === '200') {
|
||||||
|
// 갱신 성공 시 새로운 Access Token 가져오기
|
||||||
|
const newToken = response.resultData || getAccessTokenFromCookie() || localStorage.getItem('accessToken');
|
||||||
|
if (newToken && isTokenValid(newToken)) {
|
||||||
|
token = newToken;
|
||||||
|
// 갱신된 토큰을 localStorage에 즉시 저장
|
||||||
|
localStorage.setItem('accessToken', token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 갱신 실패 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!token || !isTokenValid(token)) {
|
if (!token || !isTokenValid(token)) {
|
||||||
dispatch({ type: 'LOGOUT' });
|
dispatch({ type: 'LOGOUT' });
|
||||||
@@ -130,12 +151,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 토큰이 쿠키에만 있고 localStorage에 없으면 저장해줌 (일관성 유지)
|
// 토큰이 존재하고 유효한 경우, localStorage와 동기화 (갱신 등으로 변경되었을 수 있음)
|
||||||
if (!localStorage.getItem('accessToken')) {
|
if (token && localStorage.getItem('accessToken') !== token) {
|
||||||
localStorage.setItem('accessToken', token);
|
localStorage.setItem('accessToken', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = getUserFromToken(token);
|
const decodedToken = getUserFromToken(token!); // 위에서 유효성 검사 완료됨
|
||||||
if (decodedToken) {
|
if (decodedToken) {
|
||||||
const member: Member = {
|
const member: Member = {
|
||||||
memberId: decodedToken.memberId,
|
memberId: decodedToken.memberId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate, Navigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
CButton,
|
CButton,
|
||||||
CCard,
|
CCard,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
CInputGroup,
|
CInputGroup,
|
||||||
CInputGroupText,
|
CInputGroupText,
|
||||||
CRow,
|
CRow,
|
||||||
|
CSpinner,
|
||||||
} from '@coreui/react'
|
} from '@coreui/react'
|
||||||
import CIcon from '@coreui/icons-react'
|
import CIcon from '@coreui/icons-react'
|
||||||
import { cilLockLocked, cilUser } from '@coreui/icons'
|
import { cilLockLocked, cilUser } from '@coreui/icons'
|
||||||
|
|||||||
Reference in New Issue
Block a user