React 受保护路由(Protected Route)
一、受保护路由的核心概念
受保护路由(也叫鉴权路由),是 React 路由开发中高频必备功能,核心作用:对指定的路由页面做访问权限校验,只有满足指定条件(最常见的是「用户已登录」)的情况下,才允许进入该路由页面;如果不满足条件,则强制跳转到指定页面(通常是登录页),拒绝访问。
常见使用场景:
- 后台管理系统的首页、用户列表、数据报表等页面,必须登录后才能访问;
- 个人中心、订单页面、购物车等页面,需要用户登录鉴权;
- 有权限分级的场景,比如「管理员路由」只有管理员账号能访问,普通用户访问会被拦截。
二、前置前提
- 本次所有代码都适配 HashRouter / BrowserRouter,二者完全通用,无区别;
- 所有示例基于 React Router v6 版本(目前开发唯一主流版本),v6 对受保护路由的写法做了精简优化;
- 路由鉴权的核心判断依据:前端存储的用户登录状态,最常用的是
localStorage(持久化,刷新页面不丢失),也可以用sessionStorage或 React 状态管理工具(redux/zustand)。
三、受保护路由的实现核心思路(固定逻辑,通用所有场景)
受保护路由本质是 封装一个「鉴权高阶组件」(ProtectedRoute),这个组件是对原生 <Route> 的一层封装,核心执行逻辑只有3步,固定不变:
- 接收两个核心参数:需要鉴权的目标组件
element、需要匹配的路由地址path; - 内部做权限判断:校验用户是否满足访问条件(比如:是否存在登录token、是否是管理员);
- 路由分发逻辑:
- 满足条件:正常渲染「目标组件」,允许访问该路由;
- 不满足条件:调用
navigate强制跳转到指定页面(如/login登录页),并阻止原路由的渲染。
四、核心API依赖
实现受保护路由,只需要用到 React Router v6 的2个核心钩子,都是你已经掌握的内容,无新API:
useNavigate:用于权限校验失败时,编程式路由跳转(跳登录页);useLocation:可选,优化体验,记录用户「原本要访问的受保护路由地址」,登录成功后可以回跳到该地址(非常实用的细节优化)。
五、完整实现步骤
步骤1:封装通用的「受保护路由组件」(一次封装,全局复用)
新建文件:src/components/ProtectedRoute.jsx,这是通用组件,项目中所有需要鉴权的路由,都用这个组件替代原生 <Route> 即可,封装一次,终身使用。
// 通用受保护路由组件 - 核心封装
import { Navigate, useLocation } from 'react-router-dom';
// 接收props:path(路由地址)、element(需要鉴权的组件)
const ProtectedRoute = ({ element }) => {
// 1. 获取当前要访问的路由地址(用于登录后回跳)
const location = useLocation();
// 2. 核心:用户登录状态校验(项目中统一写这里,修改一次即可)
// 从localStorage获取登录标识,存在则代表已登录,不存在则未登录
const token = localStorage.getItem('token');
// 扩展:也可以加其他权限校验,比如 const isAdmin = localStorage.getItem('role') === 'admin'
const isAuth = !!token; // 转为布尔值,有token=true,无token=false
// 3. 鉴权核心逻辑
if (isAuth) {
// 已登录,正常渲染需要鉴权的组件
return element;
} else {
// 未登录,强制跳转到登录页,并携带原本要访问的地址
// state: { from: location } 把原地址存入路由状态中
return <Navigate to="/login" state={{ from: location }} replace />;
}
};
export default ProtectedRoute;
关键细节说明:
<Navigate>是 React Router v6 的内置组件,作用是「路由重定向」,等价于编程式的navigate()跳转;replace属性:表示替换当前路由历史记录,避免用户跳转登录页后,点击浏览器回退按钮又回到原路由,优化体验;state={{ from: location }}:把用户原本要访问的路由地址存入路由的state中,登录成功后可以读取这个地址实现「回跳」。
步骤2:准备基础页面组件(示例用)
准备4个基础页面,满足演示需求,真实项目替换为自己的页面即可:
// src/pages/Login.jsx 登录页
const Login = () => {
const handleLogin = () => {
// 模拟登录逻辑:登录成功后,往localStorage存入token(登录标识)
localStorage.setItem('token', 'user-login-token-123456');
// 登录成功后跳转到首页
window.location.href = '/';
};
return (
<div>
<h2>登录页面</h2>
<p>请登录后才能访问后台页面</p>
<button onClick={handleLogin}>点击模拟登录</button>
</div>
);
};
export default Login;
// src/pages/Admin.jsx 受保护的后台首页(必须登录才能访问)
const Admin = () => {
return <div>后台管理首页 - 已登录用户可见</div>;
};
export default Admin;
// src/pages/Home.jsx 公开首页(无需登录,所有人可访问)
const Home = () => {
return <div>公开首页 - 无需登录即可访问</div>;
};
export default Home;
// src/pages/NotFound.jsx 404页面
const NotFound = () => {
return <div>404 - 页面不存在</div>;
};
export default NotFound;
步骤3:在路由配置中使用受保护路由
在 App.jsx 中配置路由,规则明确:
- 公开路由(无需鉴权):继续使用原生
<Route>组件; - 受保护路由(需要登录/权限):使用我们封装的
<ProtectedRoute>组件,嵌套在<Route>内部。
这种写法是 React Router v6 官方推荐的规范写法,兼容性最强,无任何副作用。
// src/App.jsx 路由总配置
import { Routes, Route, Link } from 'react-router-dom';
import ProtectedRoute from './components/ProtectedRoute';
import Home from './pages/Home';
import Login from './pages/Login';
import Admin from './pages/Admin';
import NotFound from './pages/NotFound';
function App() {
return (
<div className="App">
{/* 路由导航 */}
<div style={{ margin: '20px' }}>
<Link to="/" style={{ marginRight: '20px' }}>公开首页</Link>
<Link to="/admin" style={{ marginRight: '20px' }}>后台管理(需登录)</Link>
<Link to="/login">登录页</Link>
</div>
{/* 核心路由配置 */}
<Routes>
{/* ✅ 公开路由:无需鉴权,直接用Route */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* ✅ 受保护路由:必须登录才能访问,用封装的ProtectedRoute */}
<Route
path="/admin"
element={<ProtectedRoute element={<Admin />} />}
/>
{/* 404兜底路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
export default App;
步骤4:优化登录页 - 实现「登录成功后回跳」(实用优化,必加)
这是非常重要的体验优化:用户访问 /admin 时,因为未登录被跳转到 /login,登录成功后,自动跳回用户原本要访问的 /admin 页面,而不是固定跳首页。
修改 src/pages/Login.jsx,新增回跳逻辑,用到 useLocation 和 useNavigate 钩子:
// 优化后的登录页 - 带回跳功能
import { useLocation, useNavigate } from 'react-router-dom';
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
// 获取路由状态中存储的「原访问地址」,没有则默认跳首页
const fromPath = location.state?.from?.pathname || '/';
const handleLogin = () => {
// 1. 模拟登录,存入登录标识
localStorage.setItem('token', 'user-login-token-123456');
// 2. 登录成功,跳回原本要访问的地址
navigate(fromPath, { replace: true });
};
// 退出登录方法(可选,测试用)
const handleLogout = () => {
localStorage.removeItem('token');
navigate('/login');
};
return (
<div>
<h2>登录页面</h2>
<p>请登录后才能访问后台页面</p>
<button onClick={handleLogin}>点击模拟登录</button>
<button onClick={handleLogout} style={{ marginLeft: '10px' }}>退出登录</button>
</div>
);
};
export default Login;
六、扩展:多角色权限路由
上面的示例是「登录/未登录」的基础鉴权,实际开发中,很多项目有角色权限分级(比如:普通用户、管理员、超级管理员),不同角色能访问的路由不同。
这种需求只需要修改受保护路由组件的「权限判断逻辑」 即可,组件的封装结构完全不变,非常灵活。
示例:只有「管理员」能访问 /admin 路由,普通用户访问被拦截
修改 src/components/ProtectedRoute.jsx:
import { Navigate, useLocation } from 'react-router-dom';
const ProtectedRoute = ({ element, requiredRole = 'user' }) => {
const location = useLocation();
// 1. 获取登录标识和用户角色
const token = localStorage.getItem('token');
const userRole = localStorage.getItem('role'); // 存储的角色:admin / user
const isAuth = !!token;
// 2. 多角色权限判断:已登录 + 角色匹配
const hasPermission = isAuth && userRole === requiredRole;
if (hasPermission) {
return element;
} else if (isAuth) {
// 已登录,但角色不匹配,跳转到无权限页面
return <Navigate to="/no-permission" replace />;
} else {
// 未登录,跳登录页
return <Navigate to="/login" state={{ from: location }} replace />;
}
};
export default ProtectedRoute;
路由配置中指定需要的角色
// 只有角色为 admin 的用户才能访问
<Route
path="/admin"
element={<ProtectedRoute element={<Admin />} requiredRole="admin" />}
/>
登录时存入角色即可:
// 管理员登录
localStorage.setItem('token', 'admin-token');
localStorage.setItem('role', 'admin');
// 普通用户登录
localStorage.setItem('token', 'user-token');
localStorage.setItem('role', 'user');
七、受保护路由的嵌套路由适配
如果你的项目用到了嵌套路由(比如 /admin/user、/admin/order),受保护路由依然适用,且写法无变化,只需要把父路由配置为受保护路由即可,子路由会自动继承父路由的鉴权规则。
示例:
import { Outlet } from 'react-router-dom';
// 父组件 Admin.jsx
const Admin = () => {
return (
<div>
<h2>后台管理首页</h2>
<div>
<Link to="/admin/user">用户管理</Link>
<Link to="/admin/order" style={{ marginLeft: '10px' }}>订单管理</Link>
</div>
{/* 子路由出口 */}
<Outlet />
</div>
);
};
// 子组件 User.jsx / Order.jsx
const User = () => <div>用户管理页面 - 仅管理员可见</div>;
const Order = () => <div>订单管理页面 - 仅管理员可见</div>;
路由配置:
<Routes>
<Route
path="/admin"
element={<ProtectedRoute element={<Admin />} requiredRole="admin" />}
>
{/* 子路由自动继承鉴权,无需再次封装 */}
<Route path="user" element={<User />} />
<Route path="order" element={<Order />} />
</Route>
</Routes>
特点:父路由 /admin 是受保护的,那么所有子路由都会被自动保护,无需给每个子路由单独配置鉴权,简洁高效。
八、核心注意事项
- 受保护路由的权限校验仅在前端生效,是为了提升用户体验,不能替代后端的接口鉴权。后端所有敏感接口,依然需要校验请求头中的 token,防止用户通过手动修改前端存储的token绕过权限。
- 登录状态建议存在
localStorage,持久化存储,刷新页面不会丢失;如果是临时登录(关闭页面即退出),可以存在sessionStorage。 - React Router v6 中,不要再用自定义路由表+循环渲染的方式实现受保护路由,官方推荐的
<Route>嵌套<ProtectedRoute>是最稳定的写法,无兼容性问题。 - 退出登录时,一定要清除本地存储的登录标识(
localStorage.removeItem('token')),否则用户退出后依然能访问受保护路由。
九、总结
- React 受保护路由的本质是封装鉴权高阶组件,对原生
<Route>做一层权限校验的封装; - 核心逻辑固定:校验状态 → 有权限则渲染组件,无权限则重定向;
- 基础鉴权(登录/未登录)和多角色鉴权,只是校验逻辑不同,组件封装结构完全一致;
- 受保护路由完美兼容嵌套路由,父路由鉴权后,子路由自动继承权限规则;
- 所有写法基于 React Router v6 官方规范,可直接在项目中复用,无冗余代码。