路由守卫是前端权限控制的核心环节,用于在用户访问某个路由之前进行拦截校验,决定允许通过、重定向还是降级展示。在 React Router v6 中,没有内置的“守卫”钩子,但我们可以通过封装组件 + 编程式判断来实现灵活的权限控制体系。
12.4.1 典型场景
- 认证守卫:未登录用户无法访问特定页面,自动跳转到登录页。
- 角色/权限守卫:已登录但权限不足时,展示 403 无权限页面或重定向到首页。
- 路由菜单过滤:根据权限动态生成侧边栏菜单,隐藏无权访问的入口。
12.4.2 实现基础:封装受保护路由组件
通常会创建一个 ProtectedRoute 组件,接收权限校验条件,根据结果渲染目标组件或重定向。
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ isAllowed, redirectPath = '/login', children }) {
const location = useLocation();
if (!isAllowed) {
// 将用户试图访问的路径通过 state 传递,登录后可跳转回来
return <Navigate to={redirectPath} replace state={{ from: location }} />;
}
return children;
}
isAllowed 可以是一个布尔值或一个函数,由外层根据认证状态和权限规则计算而来。
认证守卫示例
function AuthRoute({ children }) {
const { user } = useAuth(); // 自定义 hook,从 Context 或状态管理中获取用户信息
const isLoggedIn = !!user;
return (
<ProtectedRoute isAllowed={isLoggedIn} redirectPath="/login">
{children}
</ProtectedRoute>
);
}
在路由配置中使用:
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/dashboard" element={
<AuthRoute>
<Dashboard />
</AuthRoute>
} />
</Routes>
12.4.3 权限控制:基于角色或权限点
简单的登录守卫只能区分“已登录/未登录”,实际项目往往需要更细粒度的权限(如管理员、普通用户、编辑者等)。将权限信息(角色列表、权限标识)存储在用户状态中,然后在路由上附加权限要求。
定义权限标识
const ROLES = {
ADMIN: 'admin',
EDITOR: 'editor',
USER: 'user',
};
// 路由配置扩展 meta 信息
const routes = [
{
path: '/admin',
element: <AdminPanel />,
requiredRoles: [ROLES.ADMIN],
},
{
path: '/editor',
element: <EditorPanel />,
requiredRoles: [ROLES.EDITOR, ROLES.ADMIN],
},
{
path: '/profile',
element: <Profile />,
requiredRoles: [], // 任何已登录用户
},
];
权限校验组件
function RoleBasedRoute({ children, requiredRoles }) {
const { user } = useAuth();
const userRoles = user?.roles || [];
const hasRequiredRole = requiredRoles.length === 0
? true
: requiredRoles.some(role => userRoles.includes(role));
return (
<ProtectedRoute isAllowed={!!user && hasRequiredRole} redirectPath="/403">
{children}
</ProtectedRoute>
);
}
然后渲染路由时动态包裹:
<Routes>
{routes.map(route => (
<Route
key={route.path}
path={route.path}
element={
<RoleBasedRoute requiredRoles={route.requiredRoles || []}>
{route.element}
</RoleBasedRoute>
}
/>
))}
</Routes>
如果用户权限不足,重定向到 /403 页面告知“无权限”,比直接跳转登录更友好。
12.4.4 动态生成侧边栏菜单
权限控制不只在路由访问时生效,也应在 UI 上体现——用户看不到自己无权访问的菜单项。
function SidebarMenu() {
const { user } = useAuth();
const userRoles = user?.roles || [];
const menuItems = [
{ label: '仪表盘', path: '/dashboard', roles: [] }, // 所有人可见
{ label: '管理面板', path: '/admin', roles: [ROLES.ADMIN] },
{ label: '编辑面板', path: '/editor', roles: [ROLES.EDITOR, ROLES.ADMIN] },
];
const filteredItems = menuItems.filter(item =>
item.roles.length === 0 || item.roles.some(role => userRoles.includes(role))
);
return (
<nav>
{filteredItems.map(item => (
<Link key={item.path} to={item.path}>{item.label}</Link>
))}
</nav>
);
}
12.4.5 集中式权限配置与高阶组件
更工程化的做法是在路由配置中集中声明权限,然后通过一个统一的工厂函数或高阶组件生成最终的路由元素。
function withPermission(Component, requiredRoles) {
return function PermissionComponent() {
const { user } = useAuth();
const userRoles = user?.roles || [];
const hasPermission = requiredRoles.length === 0
|| requiredRoles.some(r => userRoles.includes(r));
if (!user) return <Navigate to="/login" replace />;
if (!hasPermission) return <Navigate to="/403" replace />;
return <Component />;
};
}
// 使用
const AdminPage = withPermission(AdminPanel, [ROLES.ADMIN]);
这种方式让路由配置更简洁,权限逻辑与路由声明解耦。
12.4.6 小技巧与注意事项
- 登录后回跳:在跳转登录页时,通过
state保存来源路径,登录成功后使用useNavigate跳转回state.from或默认页。 - 前端权限仅作体验优化:真正的安全防线在后端,所有敏感接口必须进行权限校验,前端路由守卫无法防范直接 URL 访问或 API 调用。
- 异步权限获取:如果权限信息是从接口获取的,在应用初始化时先展示 loading,待权限就绪再渲染路由,否则会出现闪烁或被误拦截。
- 嵌套路由:对于嵌套路由,守卫可以放在父级
<Route>的element中,这样所有子路由都会受到保护。 - 多种权限模型:除了角色,还可以使用策略式权限点(如
canEditPost),将其作为requiredPermissions数组匹配。
通过灵活组合 ProtectedRoute、角色/权限校验、动态菜单过滤,就可以搭建起一套健壮且用户友好的前端权限控制体系。这套方案可随项目规模平滑演进。