Files
leaudit-platform-frontend/auth_doc/权限粒度设计方案对比.md
2025-12-05 00:09:32 +08:00

14 KiB
Raw Permalink Blame History

权限粒度设计方案对比

📋 问题描述

场景

  • 用户访问 /prompts 列表页
  • 后端返回该路由权限:['prompt_template:delete:delete', 'prompt_template:list:read']
  • 子路由 /prompts/new 的权限:['prompt_template:create:write', 'prompt_template:detail:read', 'prompt_template:update:write']

问题

  • /prompts 页面想显示"新增"和"编辑"按钮
  • 但这些按钮需要的权限(create:write, update:write)只在子路由中
  • 导致父路由无法显示这些按钮

🎯 解决方案对比

方案1:后端聚合权限 (推荐)

设计思路

后端在返回父路由权限时,自动聚合所有子路由的权限

后端返回格式

{
  "/prompts": [
    "prompt_template:list:read",      // 自身权限
    "prompt_template:delete:delete",  // 自身权限
    "prompt_template:detail:read",    // ← 从子路由聚合
    "prompt_template:create:write",   // ← 从子路由聚合
    "prompt_template:update:write"    // ← 从子路由聚合
  ],
  "/prompts/new": [
    "prompt_template:create:write",
    "prompt_template:detail:read",
    "prompt_template:update:write"
  ]
}

后端实现建议

PostgreSQL 示例

-- 递归查询:获取路由及其所有子路由的权限
WITH RECURSIVE route_permissions AS (
  -- 基础查询:当前路由的权限
  SELECT
    r.id,
    r.route_path,
    rp.permission_id,
    p.permission_key
  FROM routes r
  LEFT JOIN route_permissions rp ON r.id = rp.route_id
  LEFT JOIN permissions p ON rp.permission_id = p.id
  WHERE r.id = $1  -- 当前路由ID

  UNION

  -- 递归查询:所有子路由的权限
  SELECT
    r.id,
    r.route_path,
    rp.permission_id,
    p.permission_key
  FROM routes r
  INNER JOIN route_permissions base_rp ON r.parent_id = base_rp.id
  LEFT JOIN route_permissions rp ON r.id = rp.route_id
  LEFT JOIN permissions p ON rp.permission_id = p.id
)
SELECT DISTINCT permission_key
FROM route_permissions
WHERE permission_key IS NOT NULL
ORDER BY permission_key;

Node.js 后端逻辑示例

// 递归聚合子路由权限
function aggregatePermissions(route, allRoutes) {
  const permissions = new Set(route.permissions || []);

  // 找到所有子路由
  const children = allRoutes.filter(r => r.parent_id === route.id);

  // 递归聚合子路由权限
  children.forEach(child => {
    const childPermissions = aggregatePermissions(child, allRoutes);
    childPermissions.forEach(p => permissions.add(p));
  });

  return Array.from(permissions);
}

// 在返回路由数据前处理
routes.forEach(route => {
  route.permissions = aggregatePermissions(route, routes);
});

优点

前端逻辑简单 - 直接使用当前路由权限,无需额外处理 性能最好 - 无需前端计算,权限数据已准备好 符合直觉 - 在列表页能看到的操作,用户都有权限执行 易于维护 - 权限逻辑集中在后端,前端无需关心聚合规则

缺点

需要后端修改 - 需要数据库查询逻辑调整


方案2:前端聚合权限 (已实现)

设计思路

前端在构建权限映射表时,自动将子路由权限聚合到父路由

前端实现(已完成)

// app/api/auth/user-routes.ts

export function buildPermissionMap(
  routes: BackendRouteInfo[],
  aggregateChildren: boolean = true  // ← 默认开启聚合
): PermissionMap {
  const permissionMap = new Map<string, string[]>();

  // 递归收集路由及其所有子路由的权限
  function collectAllPermissions(route: BackendRouteInfo): string[] {
    const allPermissions = new Set<string>();

    // 添加当前路由的权限
    if (route.permissions && route.permissions.length > 0) {
      route.permissions.forEach(p => allPermissions.add(p));
    }

    // 递归收集子路由的权限
    if (aggregateChildren && route.children && route.children.length > 0) {
      route.children.forEach(child => {
        const childPermissions = collectAllPermissions(child);
        childPermissions.forEach(p => allPermissions.add(p));
      });
    }

    return Array.from(allPermissions);
  }

  function traverse(routeList: BackendRouteInfo[]) {
    for (const route of routeList) {
      const permissions = aggregateChildren
        ? collectAllPermissions(route)  // ← 聚合子路由权限
        : (route.permissions || []);

      if (permissions.length > 0) {
        permissionMap.set(route.route_path, permissions);
      }

      if (route.children && route.children.length > 0) {
        traverse(route.children);
      }
    }
  }

  traverse(routes);
  return permissionMap;
}

使用方式

// 默认开启聚合(推荐)
const permissionMap = buildPermissionMap(routes);

// 关闭聚合(仅使用路由自身权限)
const permissionMap = buildPermissionMap(routes, false);

效果

后端返回

{
  "/prompts": {
    "permissions": ["prompt_template:list:read", "prompt_template:delete:delete"]
  },
  "/prompts/new": {
    "permissions": ["prompt_template:create:write", "prompt_template:detail:read"]
  }
}

前端聚合后

{
  "/prompts": [
    "prompt_template:list:read",
    "prompt_template:delete:delete",
    "prompt_template:create:write",   // ← 从 /prompts/new 聚合
    "prompt_template:detail:read"     // ← 从 /prompts/new 聚合
  ],
  "/prompts/new": [
    "prompt_template:create:write",
    "prompt_template:detail:read"
  ]
}

优点

无需后端改动 - 前端自动处理 立即可用 - 已实现,默认开启 灵活控制 - 可通过参数开关聚合行为

缺点

前端计算开销 - 需要递归遍历路由树(但性能影响极小) 可能过度授权 - 子路由权限不一定适用于父路由场景


方案3:按资源模块返回权限 (最佳长期方案)

设计思路

核心理念:权限应该是按资源模块组织,而不是按路由组织。

后端返回格式(推荐)

{
  "routes": [
    {
      "route_path": "/prompts",
      "route_name": "prompts",
      "module": "prompt_template",  // ← 标识资源模块
      "permissions": [
        "prompt_template:list:read",
        "prompt_template:detail:read",
        "prompt_template:create:write",
        "prompt_template:update:write",
        "prompt_template:delete:delete"
      ]
    },
    {
      "route_path": "/prompts/new",
      "route_name": "prompts.new",
      "module": "prompt_template",  // ← 同一模块
      "permissions": [
        "prompt_template:create:write",
        "prompt_template:detail:read",
        "prompt_template:update:write"
      ]
    }
  ]
}

设计原则

  1. 模块级权限 - 同一资源模块的所有路由共享权限池
  2. 父路由包含全集 - 父路由(如 /prompts)应该包含该模块的所有用户权限
  3. 子路由可以限制 - 子路由(如 /prompts/:id/edit)可以是父路由权限的子集

数据库设计建议

当前表结构问题

-- 当前:权限和路由是多对多关系
route_permissions (route_id, permission_id)

推荐表结构

-- 推荐:权限和模块是多对多关系
module_permissions (
  module_name VARCHAR(100),      -- 如 'prompt_template'
  permission_id INT,
  grant_type VARCHAR(10)         -- 'GRANT' | 'DENY'
)

-- 路由表添加模块字段
routes (
  id INT,
  route_path VARCHAR(255),
  module_name VARCHAR(100),      -- ← 新增:关联到模块
  ...
)

-- 用户的模块权限(通过角色)
user_module_permissions (
  user_id INT,
  module_name VARCHAR(100),
  permission_keys TEXT[]         -- 聚合后的权限列表
)

后端查询示例

-- 获取用户在指定模块的所有权限
SELECT DISTINCT p.permission_key
FROM sso_users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE u.id = $1
  AND p.module = $2  -- 如 'prompt_template'
  AND rp.grant_type = 'GRANT'
ORDER BY p.permission_key;

前端使用方式

// 在任何提示词相关页面,都使用同一套权限
const { canCreate, canUpdate, canDelete } = usePermission();

const canCreateTemplate = canCreate('prompt_template');
const canEditTemplate = canUpdate('prompt_template');
const canDeleteTemplate = canDelete('prompt_template');

// 无论在 /prompts 还是 /prompts/new,权限检查结果一致

优点

语义清晰 - 权限围绕业务模块组织,易于理解 避免重复 - 不需要为每个路由单独配置权限 易于扩展 - 新增路由自动继承模块权限 符合RBAC原则 - 权限基于资源而非路由

缺点

需要重构数据库 - 需要调整表结构和查询逻辑 实施成本高 - 需要后端和前端配合改造


📊 方案对比总结

特性 方案1:后端聚合 方案2:前端聚合 方案3:模块权限
实施难度 中等 简单 较高
性能
维护性
扩展性
是否需要后端改动 是(较大)
当前可用性 需要实施 已实现 需要重构

🎯 推荐方案

短期方案(立即可用):方案2 - 前端聚合

理由

  • 已经实现,默认开启
  • 无需后端改动,立即解决问题
  • 可以通过参数控制聚合行为

使用方式

// 已默认开启,无需任何修改
// 访问 /prompts 时,会自动包含 /prompts/new 的权限

验证效果

// 在 /prompts 页面
const { getCurrentPermissions } = usePermission();

console.log(getCurrentPermissions());
// 输出:
// [
//   "prompt_template:list:read",
//   "prompt_template:delete:delete",
//   "prompt_template:create:write",   // ← 从子路由聚合
//   "prompt_template:detail:read",    // ← 从子路由聚合
//   "prompt_template:update:write"    // ← 从子路由聚合
// ]

长期方案(推荐):方案3 - 模块权限

实施步骤

阶段1:数据库改造

  1. routes 表添加 module_name 字段
  2. 创建 module_permissions 视图
  3. 更新权限查询逻辑

阶段2:后端API调整

  1. 修改 /rbac/user/routes 接口
  2. 按模块聚合权限,而非按路由
  3. 确保父路由包含模块的所有权限

阶段3:前端适配(无需改动)

  • 前端已支持权限聚合,自动兼容新格式

💡 最佳实践建议

1. 权限粒度原则

不推荐

{
  "/prompts": ["prompt_template:list:read"],
  "/prompts/new": ["prompt_template:create:write"],
  "/prompts/:id/edit": ["prompt_template:update:write"]
}

问题:权限分散,父路由无法显示子路由相关操作

推荐

{
  "/prompts": [
    "prompt_template:list:read",
    "prompt_template:create:write",
    "prompt_template:update:write",
    "prompt_template:delete:delete"
  ],
  "/prompts/new": ["prompt_template:create:write"],
  "/prompts/:id/edit": ["prompt_template:update:write"]
}

优点:父路由包含全集,子路由根据需要限制

2. 路由设计原则

  • 列表页(如 /documents):包含该模块的所有权限
  • 详情页(如 /documents/:id):至少包含 read 权限
  • 新增页(如 /documents/new):包含 create 权限
  • 编辑页(如 /documents/:id/edit):包含 update 权限

3. 权限命名规范

遵循 module:resource:action 格式:

prompt_template:list:read          ← 查看列表
prompt_template:detail:read        ← 查看详情
prompt_template:create:write       ← 创建
prompt_template:update:write       ← 更新
prompt_template:delete:delete      ← 删除
prompt_template:export:execute     ← 导出(特殊操作)

4. 权限继承规则

父路由权限 ⊇ 子路由权限

示例:

/prompts 权限 = [list:read, detail:read, create:write, update:write, delete:delete]
  ├─ /prompts/new 权限 = [create:write, detail:read]
  └─ /prompts/:id/edit 权限 = [update:write, detail:read]

🔧 当前系统配置

已启用:前端自动聚合

文件app/api/auth/user-routes.ts

// 默认开启子路由权限聚合
const permissionMapObj = permissionMapToObject(
  buildPermissionMap(routes)  // aggregateChildren = true (默认)
);

如何关闭聚合(不推荐)

如果需要关闭自动聚合(仅使用路由自身权限):

// app/api/auth/user-routes.ts

const permissionMapObj = permissionMapToObject(
  buildPermissionMap(routes, false)  // ← 关闭聚合
);

📝 总结

当前状态

已实现方案2 - 前端自动聚合子路由权限 立即可用 - 无需任何配置 解决问题 - /prompts 页面现在可以正确显示"新增"和"编辑"按钮

下一步建议

  1. 短期:使用当前的前端聚合方案(已完成)
  2. 中期:与后端沟通,考虑在后端实现聚合(方案1)
  3. 长期:规划模块化权限设计(方案3

注意事项

⚠️ 前端权限检查仅用于UI控制 ⚠️ 后端必须验证所有操作的权限 ⚠️ 定期审查权限配置的合理性


文档版本: 1.0.0 创建日期: 2025-11-27 作者: Claude Code