之前项目的路由保护是这样的:每个需要登录的页面都判断一下token,没有就跳转登录页。
// 之前:每个页面都要写 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里套一个全局的:
// main.jsx <BrowserRouter> <ProtectedRoute> {/* 全局保护,只套一次 */} <ConfigProvider> <App /> </ConfigProvider> </ProtectedRoute> </BrowserRouter>这样<App />里面的所有路由都会经过保护逻辑。
ProtectedRoute 的实现
ProtectedRoute组件要做三件事:
- 检查 token,没有就跳登录
- 检查路由是否合法(防止访问不存在的页面)
- 处理登录后跳回原页面的逻辑
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 拦截器里:
// 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标志位,确保只弹一次:
export let isRelogin = { show: false }; if (response.data.code === 401) { if (!isRelogin.show) { // 只处理第一次 isRelogin.show = true; message.error('登录状态已过期'); window.location.href = "/login"; } }登录成功后,记得重置这个标志位:
// 登录成功后 isRelogin.show = false;懒加载怎么处理?
React Router 7 推荐用懒加载,但ProtectedRoute会阻止懒加载的组件渲染。
我的方案是:只懒加载页面组件,不懒加载ProtectedRoute。
// 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保存