权限控制
XinAdmin 采用双端认证机制,支持管理员(sys_users)和普通用户(users)两种用户类型。权限控制系统实现了细粒度的访问控制,包括菜单级、路由级和按钮级权限验证。
权限控制架构
XinAdmin 的权限控制分为以下几个层面:
- 后端权限验证:基于角色的访问控制(RBAC),通过
sys_role、sys_rule 和 sys_role_rule 表实现权限分配
- 前端权限展示:通过
AuthButton、AuthRoute 等组件控制界面元素的可见性
- 动态菜单生成:根据用户权限动态生成导航菜单
- API请求拦截:在请求层面进行权限验证
后端权限机制
权限表结构
sys_user:系统用户表
sys_role:角色表
sys_user_role:用户角色关联表
sys_rule:权限规则表 (type: menu/route/nested-route/rule)
sys_role_rule:角色权限关联表
权限验证流程
- 用户登录时,后端会根据用户的角色查询其拥有的权限规则
- 将权限列表存储在 Token 的 abilities 中
- 每次请求时验证 Token 和权限
- 返回用户信息时同时返回权限列表
前端权限实现
权限状态管理
前端通过 Zustand 状态管理库中的 useAuthStore 管理权限相关状态,主要包括:
user:当前登录用户信息
access:用户权限列表,以数组形式存储
权限列表会在用户登录后从后端获取,并存储在 access 数组中。例如:
// 示例权限列表
const access = [
'sys-user.list',
'sys-user.create',
'sys-user.update',
'sys-user.delete',
'system.dict.list',
'system.dict.create',
'system.dict.update',
'system.dict.delete'
];
权限验证组件
XinAdmin 提供了专门的权限验证组件来控制界面元素的显示:
AuthButton 组件
AuthButton 组件用于控制按钮级别的权限,当用户不具备指定权限时,按钮不会显示。
基本用法:
import AuthButton from '@/components/AuthButton';
// 仅当用户拥有 'sys-user.create' 权限时显示新增按钮
<AuthButton auth="sys-user.create">
<Button type="primary">新增用户</Button>
</AuthButton>
组件属性:
auth:权限标识符,对应后端权限规则的名称
children:需要进行权限验证的子元素
内部实现:
const AuthButton = ({ auth, children }: AuthButtonProps) => {
const access = useAuthStore(state => state.access);
const hasPermission = useMemo(() => {
// 未指定权限,默认显示
if (!auth) return true;
// 检查权限
return access.includes(auth);
}, [access, auth]);
// 无权限时不渲染
if (!hasPermission) return null;
return <>{children}</>;
};
AuthRoute 组件
AuthRoute 组件用于路由级别的权限控制,主要用来保护需要登录才能访问的页面。
基本用法:
import AuthRoute from '@/components/AuthRoute';
// 只有登录用户才能访问仪表板页面
<AuthRoute>
<Dashboard />
</AuthRoute>
组件属性:
children:需要进行权限验证的子元素
redirectTo:未登录时重定向的目标路径,默认为 /login
XinTable 组件中的权限控制
XinTable 组件集成了权限控制功能,通过 accessName 属性实现对表格操作按钮的权限控制。
基本用法:
<XinTable
accessName="sys-user.list"
// 其他属性...
/>
权限映射:
accessName + '.create':新增权限,控制新增按钮的显示
accessName + '.update':编辑权限,控制编辑按钮的显示
accessName + '.delete':删除权限,控制删除按钮的显示
内部实现示例:
// XinTable 内部对新增按钮的权限控制
<AuthButton auth={accessName + '.create'} key={'create'}>
<Button children={t('xin-table.createButton')} type={'primary'} onClick={addButtonClick} />
</AuthButton>
// 对编辑按钮的权限控制
<AuthButton auth={props.accessName + '.update'} key={'update'}>
<Button type="primary" icon={<EditOutlined />} size={'small'} onClick={() => editButtonClick(record)} />
</AuthButton>
权限控制最佳实践
权限命名规范
权限标识通常采用模块.操作的形式,例如:
sys-user.list:用户列表查看权限
sys-user.create:用户新增权限
sys-user.update:用户编辑权限
sys-user.delete:用户删除权限
system.dict.list:字典列表查看权限
system.dict.create:字典新增权限
组件使用建议
- 按钮级权限:对于需要隐藏特定操作按钮的场景,使用
AuthButton 组件
- 路由级权限:对于需要登录才能访问的页面,使用
AuthRoute 组件
- 表格权限:对于 CRUD 操作,使用
XinTable 组件的 accessName 属性
- 自定义权限:对于复杂的权限判断,可以直接使用
useAuthStore 获取权限列表进行判断
性能优化
- 精确订阅:只订阅所需的权限状态,避免不必要的重渲染
// 好的做法
const access = useAuthStore(state => state.access);
// 或者使用选择器
const hasCreatePermission = useAuthStore(state =>
state.access.includes('sys-user.create')
);
- 缓存权限判断结果:使用
useMemo 缓存复杂的权限判断逻辑
const hasComplexPermission = useMemo(() => {
// 复杂的权限判断逻辑
return access.includes('permission1') && access.includes('permission2');
}, [access]);
权限控制常见问题
权限更新不及时
如果用户权限发生变化,需要重新登录或手动调用 getInfo 方法更新权限信息:
import { useAuthStore } from '@/stores';
const { getInfo } = useAuthStore();
await getInfo(); // 重新获取用户权限信息
权限控制失效
检查以下几点:
- 权限标识符是否正确
- 用户是否真正拥有该权限
- 权限标识符格式是否符合约定
- 组件是否正确嵌套在权限上下文中