2025. 2. 21. 00:33ㆍ프로젝트
프로젝트를 진행하면서, 인증이 필요한 HTTP 요청에는 JWT와 Axios Interceptor를 활용해 인증 로직을 구현했습니다.
하지만 단순한 페이지 간 라우팅에서는 사용자의 권한 여부와 관계없이 접근이 가능했기 때문에, 보안상의 허점이 발생할 수 있었습니다.
이를 해결하기 위해, React Router의 PrivateRoute를 활용해 인증이 필요한 페이지에 대한 접근을 제어하려고 합니다.
이번 글에서는 PrivateRoute를 구현하여 인증되지 않은 사용자가 보호된 페이지에 접근하지 못하도록 적용하여 그에 대해서 포스팅하겠습니다.
먼저 페이지에 대한 접근을 제어하기 위해서 아래와 같이 AppRoutes를 구성해 줍니다.
- AppRoutes
const AppRoutes = () => (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route element={<PrivateRoute />}>
<Route path="/" element={<Main />} />
<Route path="/my" element={<MyPage />} />
<Route path="/create-meeting" element={<CreateMeeting />} />
<Route path="/meeting-list" element={<MeetingList />} />
</Route>
</Routes>
);
export default AppRoutes;
Route의 element는 렌더링 할 컴포넌트를 지정하는 역할을 합니다.
권한이 필요한 경로를 감싸서 PrivateRoute라는 컴포넌트로 감싸면 해당 경로로 접근을 할 때 PrivateRoute가 먼저 실행됩니다.
그럼 이제 먼저 실행되는 PrivateRoute 컴포넌트를 구성해 봅시다.
- PrivateRoute.tsx
import { useEffect, useState } from "react";
import { Navigate, Outlet } from "react-router-dom";
import axios from "axios";
const PrivateRoute = () => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
useEffect(() => {
const checkAuth = async () => {
try {
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
setIsAuthenticated(false);
return;
}
await axios.get("/auth/validate-token", {
headers: { Authorization: `${accessToken}` },
});
setIsAuthenticated(true);
} catch (error: any) {
const errorCode = error.response?.data?.code;
if (['AUTH-001', 'AUTH-002'].includes(errorCode)) {
try {
const refreshResponse = await axios.post("auth/refresh", {}, { withCredentials: true });
const newAccessToken = refreshResponse.headers.authorization;
localStorage.setItem('accessToken', newAccessToken);
setIsAuthenticated(true);
} catch (refreshError) {
localStorage.removeItem("accessToken");
setIsAuthenticated(false);
}
}
}
};
checkAuth();
}, []);
if (isAuthenticated === null) return <div>Loading...</div>;
return isAuthenticated ? <Outlet /> : <Navigate to="/login" replace />;
};
export default PrivateRoute;
이렇게 useEffect를 이용해서 구성해 주면 됩니다.
저는 JwtFilter를 사용해서 토큰을 검증하고 있으므로 /auth/validate-token은 단순히 200 상태메시지를 응답하도록 구성했습니다.
JwtFilter를 구현하는 방법은 제 블로그의 다른 글에서 확인할 수 있습니다.
처음 컴포넌트가 렌더링 되어서 Loading 문구가 보입니다.
그다음 useEffect가 실행되어서 상태업데이트가 되고,
다시 재렌더링되어서 로그인 페이지 또는 기존 이동하려던 페이지로 라우팅 됩니다.
추가 기능 - 이동하려던 페이지로 리다이렉트
사용자가 마이페이지로 이동하던 중에 토큰이 만료되어 로그인 페이지로 라우팅 되었다고 생각해 봅시다.
여기서 다시 로그인했을 때 자신이 이동하려던 페이지로 이동되게 구현한다면,
사소한 기능이지만 사용자 경험적 측면에서는 좋은 영향을 줄 것입니다.
- PrivateRoute
import { useEffect, useState } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import axios from "axios";
const PrivateRoute = () => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const location = useLocation();
...
return isAuthenticated ? <Outlet /> : <Navigate to="/login" replace state={{ from: location }} />;
};
export default PrivateRoute;
마찬가지로 로그인 페이지도 수정해줘야 합니다.
- Login.tsx
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/";
const handleLogin = async () => {
try {
...
await api.post("/login", formData, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}).then(e => {
localStorage.setItem('accessToken', e.headers.authorization);
navigate(from, { replace: true});
})
} catch (error) {
alert("로그인 중 문제가 발생했습니다.");
}
}
return (
...
);
};
export default Login;
사용자가 원래 가려고 했던 경로를 from에 저장해 놓고 로그인에 성공했을 때 from이 있다면 해당 페이지로
아니라면 /로 라우팅합니다.
'프로젝트' 카테고리의 다른 글
S3 고아 객체 처리하기 (0) | 2025.05.16 |
---|---|
Axios Interceptor를 이용한 JWT 관리 (0) | 2025.01.08 |
Redis를 이용한 휴대폰번호 인증 구현 (0) | 2024.12.27 |