菜单规则

XinAdmin 基于 Laravel Sanctum 实现用户认证与授权,应用中的每一个接口、菜单、页面和按钮都可以看作为 Sanctum 的一个 能力(abilities),通过 菜单规则 来控制访问权限。

核心特性

  • 控制器注解路由权限验证 - 通过路由注解自动验证用户权限
  • 动态菜单 - 根据用户角色动态生成菜单
  • 页面按钮权限验证 - 基于用户权限控制页面元素显示

数据表结构

sys_rule - 规则表

存储系统所有权限规则,包括菜单、路由和按钮级别权限。

字段类型说明
idbigint规则ID
parent_idbigint父级ID,顶级菜单为 0
typevarchar规则类型:menu/route/rule
namevarchar规则名称
keyvarchar唯一标识,用于权限校验
pathvarchar路由路径,菜单的路径会被当作前缀路由
iconvarchar图标名称
localvarchar多语言标识
linktinyint是否外链:0-否,1-是
orderint排序号
statustinyint状态:0-正常,1-停用
hiddentinyint是否隐藏:0-显示,1-隐藏

type 规则类型说明:

类型说明
menu菜单类型,用于侧边栏菜单展示
route路由类型,对应具体页面,会生成路由规则
rule权限类型,用于细粒度权限控制(如按钮权限)

sys_role - 角色表

字段类型说明
idbigint角色ID
namevarchar角色名称
sortint排序号
descriptionvarchar角色描述
statustinyint状态:0-正常,1-停用

sys_role_rule - 角色权限中间表

字段类型说明
role_idbigint角色ID
rule_idbigint规则ID

权限类型详解

菜单类型(menu)

用于侧边栏菜单展示,对应系统菜单结构。

// 菜单类型示例
[
    'id' => 1,
    'parent_id' => 0,
    'type' => 'menu',
    'name' => '系统管理',
    'key' => 'system',
    'path' => '/admin/system',
    'icon' => 'SettingOutlined',
    'order' => 1,
    'status' => 0,
    'hidden' => 0,
]

路由类型(route)

对应具体页面或功能,会生成实际路由规则并参与权限校验。

// 路由类型示例
[
    'id' => 2,
    'parent_id' => 1,
    'type' => 'route',
    'name' => '用户管理',
    'key' => 'system.user',
    'path' => '/admin/system/user',
    'local' => 'menu.system.user',
    'order' => 1,
    'status' => 0,
    'hidden' => 0,
]

权限类型(rule)

用于细粒度权限控制,如页面内的按钮权限。

// 权限类型示例
[
    'id' => 10,
    'parent_id' => 2,
    'type' => 'rule',
    'name' => '导出用户',
    'key' => 'system.user.export',
    'order' => 1,
    'status' => 0,
]

权限验证流程

后端权限验证

  1. 用户登录时,系统通过 SysUserService::ruleKeys() 获取用户所有权限 key
  2. 用户访问接口时,authorize 中间件验证请求是否具有对应权限
  3. 路由注解中的 authorize 参数与权限 key 进行匹配
// 控制器示例
#[RequestAttribute(
    routePrefix: '/admin/user',
    abilitiesPrefix: 'admin'
)]
class UserController extends Controller
{
    // 需要的权限: admin.user.create
    #[PostRoute('/create', 'user.create')]
    public function create(): JsonResponse { }

    // 需要的权限: admin.user.update
    #[PutRoute('/{id}', 'user.update')]
    public function update(int $id): JsonResponse { }
}

超级管理员

用户ID为1的账号为超级管理员,拥有所有权限。

// SysUserService 中的判断逻辑
if ($id == 1) {
    // 超级管理员拥有所有权限
    return SysRuleModel::query()->where('status', 1)->pluck('key')->toArray();
}

动态菜单生成

后端实现

用户登录后调用 /system/user/menu 接口获取菜单数据:

// SysUserController::menu()
public function menu(): JsonResponse
{
    $id = Auth::id();
    $menus = $this->service->getAdminMenus($id);
    return $this->success(compact('menus'));
}

获取用户菜单逻辑

public function getAdminMenus(int $id): array
{
    // 超级管理员获取所有菜单
    if ($id == 1) {
        $menus = SysRuleModel::query()
            ->where('status', 1)
            ->whereIn('type', ['menu', 'route'])
            ->get()
            ->toArray();
    } else {
        // 普通用户通过角色获取菜单
        $roles = SysUserModel::with(['roles.rules' => function ($query) {
            $query->where('status', 1)
                  ->whereIn('type', ['menu', 'route']);
        }])->find($id)->roles;

        $menus = collect($roles)
            ->map(fn ($item) => $item['rules'])
            ->collapse()
            ->unique('id')
            ->toArray();
    }
    return $this->getTreeData($menus);
}

前端菜单渲染

前端通过 useMenuStore 管理菜单状态:

// 获取菜单
const menus = await menu();

菜单渲染时会过滤隐藏项:

const transformMenus = (nodes: IMenus[], t: any): MenuItem[] => {
  return nodes.reduce<MenuItem[]>((acc, node) => {
    // 只处理 route 和 menu 类型,且非隐藏的项
    if (!['route', 'menu'].includes(node.type!) || !node.hidden) {
      return acc;
    }
    // 构建菜单项...
  }, []);
};

权限中间件

abilities 中间件

验证用户是否具有特定权限能力:

// 使用方式
$middleware[] = 'abilities:' . $authorize;

// 例如:abilities:admin.user.create

authGuard 中间件

验证用户是否已认证:

$middleware[] = 'auth:sanctum';
$middleware[] = 'authGuard';  // 或带参数的 authGuard:admin

菜单规则管理

管理页面功能

  • 菜单列表:树形展示所有菜单规则
  • 添加规则:支持添加菜单、路由、权限三种类型
  • 编辑规则:修改规则信息
  • 删除规则:删除规则(需确认)
  • 设置显示状态:控制菜单是否在侧边栏显示
  • 设置启用状态:控制规则是否生效

添加规则示例

添加顶级菜单:

上级菜单:顶级菜单
类型:菜单
名称:内容管理
唯一标识:content
排序:10

添加子级路由:

上级菜单:内容管理
类型:路由
名称:文章管理
唯一标识:content.article
路由路径:/admin/content/article
多语言:menu.content.article
排序:1

添加按钮权限:

上级菜单:文章管理
类型:权限
名称:审核文章
唯一标识:content.article.audit
排序:1

与注解路由的关联

菜单规则与注解路由配合使用,权限 key 的拼接规则:

组件abilitiesPrefixauthorize最终权限 key
RequestAttributeadmin--
Create-createadmin.create
Update-updateadmin.update
Query-queryadmin.query

菜单规则中的 key 字段需要与注解路由生成的权限 key 保持一致:

// 控制器
#[RequestAttribute(
    routePrefix: '/admin/user',
    abilitiesPrefix: 'admin'
)]
class UserController extends Controller
{
    #[PostRoute('/create', 'user.create')]
    public function create() { }
    // 权限: admin.user.create
}

// 菜单规则表中的对应规则
['key' => 'admin.user.create', 'name' => '创建用户']

最佳实践

1. 权限 key 命名规范

建议采用 模块.资源.操作 的命名方式:

system.user.list      - 用户列表
system.user.create    - 创建用户
system.user.update    - 更新用户
system.user.delete    - 删除用户
system.user.export    - 导出用户

2. 角色权限分配

建议创建多个角色,每个角色分配不同的权限组合:

  • 超级管理员:拥有所有权限
  • 运营管理员:内容管理相关权限
  • 普通管理员:基础查看权限

3. 按钮级权限控制

对于页面内的敏感操作(如删除、审核),使用 rule 类型权限:

// React 组件中根据权限显示按钮
{hasPermission('content.article.audit') && (
    <Button onClick={handleAudit}>审核</Button>
)}