权限组件设计
约 1402 字大约 5 分钟
布欧-Lewyon
2026-05-15
企业中后台系统几乎离不开权限控制(RBAC / ABAC)。从"只让特定角色看到某个按钮"到"根据用户属性动态决定路由/菜单/操作"。
权限模型概述
RBAC(Role-Based Access Control)
用户 → 角色 → 权限 三层模型:
ABAC(Attribute-Based Access Control)
基于用户属性 + 资源属性 + 环境条件动态判断:
允许操作 = (用户.部门 === 资源.所属部门) AND (时间在 9:00-18:00)前端权限通常走 RBAC,ABAC 更多在后端校验。
权限数据结构
前端从 /api/user/permissions 获取当前用户的权限列表:
// 权限定义
interface Permission {
code: string; // 权限标识,如 "user:create"、"order:delete"
name: string; // 显示名
type: 'menu' | 'button' | 'api';
}
interface UserAuth {
roles: string[]; // ["admin", "editor"]
permissions: string[]; // ["user:create", "order:view", ...]
menus: MenuItem[]; // 可用菜单树
}方案一:权限 Hook
最灵活的方式,在组件内直接判断权限:
// hooks/usePermission.ts
import useAuthStore from '@/stores/auth';
export function usePermission() {
const { permissions, roles } = useAuthStore();
const hasPermission = (code: string): boolean => {
// 超级管理员拥有全部权限
if (roles.includes('super_admin')) return true;
return permissions.includes(code);
};
const hasAnyPermission = (codes: string[]): boolean =>
codes.some(code => hasPermission(code));
const hasAllPermissions = (codes: string[]): boolean =>
codes.every(code => hasPermission(code));
return { hasPermission, hasAnyPermission, hasAllPermissions, roles };
}在组件中使用:
function UserList() {
const { hasPermission } = usePermission();
return (
<div>
<table>...</table>
{hasPermission('user:create') && (
<button onClick={openCreateModal}>新增用户</button>
)}
{hasPermission('user:delete') && (
<button onClick={confirmDelete}>删除</button>
)}
</div>
);
}方案二:权限组件
声明式语法,更直观:
// components/Permission.tsx
import { usePermission } from '@/hooks/usePermission';
interface PermissionProps {
code: string; // 所需权限
mode?: 'one' | 'all'; // one=任一满足, all=全部满足(默认 one)
fallback?: ReactNode; // 无权限时显示的内容
children: ReactNode;
}
function Permission({ code, mode = 'one', fallback = null, children }: PermissionProps) {
const { hasPermission, hasAnyPermission, hasAllPermissions } = usePermission();
let hasAuth: boolean;
if (typeof code === 'string') {
hasAuth = hasPermission(code);
} else if (mode === 'all') {
hasAuth = hasAllPermissions(code);
} else {
hasAuth = hasAnyPermission(code);
}
if (!hasAuth) return <>{fallback}</>;
return <>{children}</>;
}使用:
// 单权限
<Permission code="user:create">
<button>新增用户</button>
</Permission>
// 无权限时展示灰态按钮
<Permission code="order:delete" fallback={<button disabled>删除</button>}>
<button onClick={handleDelete}>删除</button>
</Permission>
// 需要同时拥有多个权限
<Permission code={['user:create', 'user:edit']} mode="all">
<UserToolbar />
</Permission>方案三:高阶组件
适合对整页或整个组件做权限保护:
// hoc/withPermission.tsx
import { usePermission } from '@/hooks/usePermission';
import { Result, Button } from 'antd';
function withPermission(code: string | string[], options?: {
redirect?: string;
fallback?: ReactNode;
}) {
return function <P extends object>(WrappedComponent: React.ComponentType<P>) {
return function PermissionHOC(props: P) {
const { hasPermission, hasAllPermissions } = usePermission();
const codes = Array.isArray(code) ? code : [code];
const hasAuth = options?.mode === 'all'
? hasAllPermissions(codes)
: codes.some(c => hasPermission(c));
if (!hasAuth) {
if (options?.fallback) return <>{options.fallback}</>;
return (
<Result
status="403"
title="403"
subTitle="抱歉,您没有访问此页面的权限"
extra={<Button type="primary" onClick={() => window.history.back()}>返回</Button>}
/>
);
}
return <WrappedComponent {...props} />;
};
};
}
// 使用
const AdminPage = withPermission('admin:access', { mode: 'all' })(AdminDashboard);方案四:路由级权限
与 React Router 配合,控制页面级别访问:
// router/AuthGuard.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { usePermission } from '@/hooks/usePermission';
interface AuthGuardProps {
children: ReactNode;
permission?: string; // 需要的页面权限
roles?: string[]; // 允许的角色
}
function AuthGuard({ children, permission, roles }: AuthGuardProps) {
const { hasPermission, roles: userRoles } = usePermission();
const location = useLocation();
// 1. 先检查角色
if (roles && !roles.some(r => userRoles.includes(r))) {
return <Navigate to="/403" state={{ from: location }} replace />;
}
// 2. 再检查权限
if (permission && !hasPermission(permission)) {
return <Navigate to="/403" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// 路由配置
const routes = [
{
path: '/dashboard',
element: <AuthGuard permission="dashboard:view"><Dashboard /></AuthGuard>,
},
{
path: '/admin',
element: <AuthGuard roles={['admin', 'super_admin']}><AdminPanel /></AuthGuard>,
},
];方案五:菜单权限过滤
根据用户权限动态过滤侧边栏菜单:
// utils/filterMenu.ts
interface MenuItem {
key: string;
label: string;
icon?: ReactNode;
permission?: string; // 需要的权限
children?: MenuItem[];
}
function filterMenu(menus: MenuItem[], hasPermission: (code: string) => boolean): MenuItem[] {
return menus
.filter(item => !item.permission || hasPermission(item.permission))
.map(item => ({
...item,
children: item.children ? filterMenu(item.children, hasPermission) : undefined,
}));
}
// 在 Layout 中使用
function Sidebar() {
const { hasPermission } = usePermission();
const filteredMenus = useMemo(
() => filterMenu(allMenus, hasPermission),
[hasPermission, allMenus]
);
return <Menu items={filteredMenus} />;
}完整架构
最佳实践
- 前端权限只做展示控制,后端必须重复校验——恶意请求可以直接绕过前端
- 权限使用字符串常量枚举,避免硬编码:
// constants/permissions.ts
export const PERM = {
USER_CREATE: 'user:create',
USER_EDIT: 'user:edit',
USER_DELETE: 'user:delete',
USER_VIEW: 'user:view',
ORDER_VIEW: 'order:view',
ORDER_EXPORT:'order:export',
} as const;- 用户信息变更(如切换角色)应重置全局 Store 并刷新权限
- 大项目建议用
zustand或redux管理权限状态,避免 Context 频繁 re-render - 权限粒度不宜过细——通常到"页面级 + 操作级(增删改查)"即可,每按钮一个 code 会难以维护
- 后端返回权限码时,建议一次性下发完整列表,前端做本地过滤,而非逐个请求校验
小结
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
usePermission Hook | 组件内灵活判断 | 零封装,最灵活 | 写法略啰嗦 |
Permission 组件 | 按钮/操作级的权限控制 | 声明式,可读性强 | 新增组件开销 |
withPermission HOC | 整页/整组件的权限包装 | 可配合 HOC 链 | 类型推导略麻烦 |
AuthGuard 路由守卫 | 页面级别访问控制 | 集成路由,体验好 | 需配置路由 |
| 菜单过滤 | 侧边栏动态生成 | 用户只看到有权限的菜单 | — |
根据项目规模和管理精细度选择 2~3 种组合使用。
上一节:复合组件与 Render Props 下一节:Sentry 错误监控
