权限控制

XinAdmin 采用双端认证机制,支持管理员(sys_users)和普通用户(users)两种用户类型。权限控制系统实现了细粒度的访问控制,包括菜单级、路由级和按钮级权限验证。

权限控制架构

XinAdmin 的权限控制分为以下几个层面:

  1. 后端权限验证:基于角色的访问控制(RBAC),通过 sys_rolesys_rulesys_role_rule 表实现权限分配
  2. 前端权限展示:通过 AuthButtonAuthRoute 等组件控制界面元素的可见性
  3. 动态菜单生成:根据用户权限动态生成导航菜单
  4. API请求拦截:在请求层面进行权限验证

后端权限机制

权限表结构

  • sys_user:系统用户表
  • sys_role:角色表
  • sys_user_role:用户角色关联表
  • sys_rule:权限规则表 (type: menu/route/nested-route/rule)
  • sys_role_rule:角色权限关联表

权限验证流程

  1. 用户登录时,后端会根据用户的角色查询其拥有的权限规则
  2. 将权限列表存储在 Token 的 abilities 中
  3. 每次请求时验证 Token 和权限
  4. 返回用户信息时同时返回权限列表

前端权限实现

权限状态管理

前端通过 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:字典新增权限

组件使用建议

  1. 按钮级权限:对于需要隐藏特定操作按钮的场景,使用 AuthButton 组件
  2. 路由级权限:对于需要登录才能访问的页面,使用 AuthRoute 组件
  3. 表格权限:对于 CRUD 操作,使用 XinTable 组件的 accessName 属性
  4. 自定义权限:对于复杂的权限判断,可以直接使用 useAuthStore 获取权限列表进行判断

性能优化

  1. 精确订阅:只订阅所需的权限状态,避免不必要的重渲染
// 好的做法
const access = useAuthStore(state => state.access);

// 或者使用选择器
const hasCreatePermission = useAuthStore(state => 
  state.access.includes('sys-user.create')
);
  1. 缓存权限判断结果:使用 useMemo 缓存复杂的权限判断逻辑
const hasComplexPermission = useMemo(() => {
  // 复杂的权限判断逻辑
  return access.includes('permission1') && access.includes('permission2');
}, [access]);

权限控制常见问题

权限更新不及时

如果用户权限发生变化,需要重新登录或手动调用 getInfo 方法更新权限信息:

import { useAuthStore } from '@/stores';

const { getInfo } = useAuthStore();
await getInfo(); // 重新获取用户权限信息

权限控制失效

检查以下几点:

  1. 权限标识符是否正确
  2. 用户是否真正拥有该权限
  3. 权限标识符格式是否符合约定
  4. 组件是否正确嵌套在权限上下文中