之前项目的路由保护是这样的:每个需要登录的页面都判断一下 token,没有就跳转登录页。
javascript
// 之前:每个页面都要写
const SomePage = () => {
const token = localStorage.getItem('token');
if (!token) {
return <Navigate to="/login" />;
}
return <div>页面内容</div>;
};
这样写有几个问题:
- 每个页面都要重复这个逻辑
- 万一漏了一个页面,就有安全漏洞
- 代码很乱,业务逻辑混在一起
后来我改成全局路由保护,清爽多了。
React Router 7 的新特性
React Router 7 推出了 <BrowserRouter> 的 basename 和一些新特性,但最实用的是嵌套路由。
但我的方案不是在每个路由外面套 <ProtectedRoute>,而是在 main.jsx 里套一个全局的:
javascript
// main.jsx
<BrowserRouter>
<ProtectedRoute> {/* 全局保护,只套一次 */}
<ConfigProvider>
<App />
</ConfigProvider>
</ProtectedRoute>
</BrowserRouter>
这样 <App /> 里面的所有路由都会经过保护逻辑。
ProtectedRoute 的实现
ProtectedRoute 组件要做三件事:
- 检查 token,没有就跳登录
- 检查路由是否合法(防止访问不存在的页面)
- 处理登录后跳回原页面的逻辑
javascript
const ProtectedRoute = ({ children }) => {
const location = useLocation();
const pathname = location.pathname;
// 1. 检查路由是否合法
const allowedRoutes = [
"/login", "/", "/chat", "/customer", "/review-dashboard", "/pdf-annotator/:id", ...
];
const isValidRoute = allowedRoutes.some(route =>
pathname === route || pathname.startsWith(`${route}/`)
);
if (!isValidRoute) {
return <Navigate to="/404" replace />;
}
// 2. 登录页面直接放行
if (pathname === "/login") {
return <>{children}</>;
}
// 3. 检查 token
const token = localStorage.getItem("token");
if (!token) {
localStorage.setItem("pathname", pathname); // 保存原路径
return <Navigate to="/login" replace />;
}
// 4. 根路径重定向
if (pathname === "/") {
return <Navigate to="/chat" replace />;
}
return <>{children}</>;
};
这样写的好处:
- 所有路由都在一个地方管理,不会漏
- 未定义的路由自动跳 404
- 登录后自动跳回原页面
但是有个坑:401 错误处理
用户登录后,token 会过期。这时候后端返回 401,我需要自动跳转到登录页。
但这个逻辑不能写在 ProtectedRoute 里,因为它只在路由切换时执行,不会响应 API 请求。
我把它写在了 Axios 拦截器里:
javascript
// request.js
export let isRelogin = { show: false };
request.interceptors.response.use(
(response) => {
if (response.data.code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
message.error('登录状态已过期');
localStorage.setItem("pathname", window.location.pathname);
localStorage.removeItem("token");
window.location.href = "/login";
}
} else if (response.data.code !== 200) {
message.error(response.data.msg);
return Promise.reject(response.data);
}
return response;
},
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
}
);
这里有个坑:如果多个请求同时返回 401,会弹出多次错误提示。
我用了个 isRelogin.show 标志位,确保只弹一次:
javascript
export let isRelogin = { show: false };
if (response.data.code === 401) {
if (!isRelogin.show) { // 只处理第一次
isRelogin.show = true;
message.error('登录状态已过期');
window.location.href = "/login";
}
}
登录成功后,记得重置这个标志位:
javascript
// 登录成功后
isRelogin.show = false;
懒加载怎么处理?
React Router 7 推荐用懒加载,但 ProtectedRoute 会阻止懒加载的组件渲染。
我的方案是:只懒加载页面组件,不懒加载 ProtectedRoute。
javascript
// App.jsx
const HomePage = lazy(() => import("./view/HomePage"));
const PDFAnnotatorDemo = lazy(() => import("./view/PDFAnnotatorDemo"));
const App = () => {
return (
<Suspense fallback={<PageLoading />}>
<Routes>
<Route path="/homepage" element={<HomePage />} />
<Route path="/pdf-annotator/:id" element={<PDFAnnotatorDemo />} />
...
</Routes>
</Suspense>
);
};
ProtectedRoute 在 main.jsx 里,不会被懒加载,所以一开始就会加载。
最后的效果
现在的路由架构:
main.jsx:全局保护 + 登录页不懒加载App.jsx:懒加载所有其他页面request.js:401 自动跳登录
代码清爽多了,也不用担心漏保护某个页面。
几个踩坑总结
- 全局保护比单独保护好 :一次套在
main.jsx里就行 - 401 处理要防重复 :用
isRelogin.show标志位 - 路由白名单要维护:未定义的路由跳 404
- 懒加载不能保护 ProtectedRoute:它要最先加载
- 登录后要跳回原路径 :用
localStorage.pathname保存