# 权限粒度设计方案对比 ## 📋 问题描述 **场景**: - 用户访问 `/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:后端聚合权限 ⭐⭐⭐⭐⭐ (推荐) #### 设计思路 后端在返回父路由权限时,**自动聚合所有子路由的权限**。 #### 后端返回格式 ```json { "/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 示例**: ```sql -- 递归查询:获取路由及其所有子路由的权限 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 后端逻辑示例**: ```javascript // 递归聚合子路由权限 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:前端聚合权限 ⭐⭐⭐⭐ (已实现) #### 设计思路 前端在构建权限映射表时,**自动将子路由权限聚合到父路由**。 #### 前端实现(已完成) ```typescript // app/api/auth/user-routes.ts export function buildPermissionMap( routes: BackendRouteInfo[], aggregateChildren: boolean = true // ← 默认开启聚合 ): PermissionMap { const permissionMap = new Map(); // 递归收集路由及其所有子路由的权限 function collectAllPermissions(route: BackendRouteInfo): string[] { const allPermissions = new Set(); // 添加当前路由的权限 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; } ``` #### 使用方式 ```typescript // 默认开启聚合(推荐) const permissionMap = buildPermissionMap(routes); // 关闭聚合(仅使用路由自身权限) const permissionMap = buildPermissionMap(routes, false); ``` #### 效果 **后端返回**: ```json { "/prompts": { "permissions": ["prompt_template:list:read", "prompt_template:delete:delete"] }, "/prompts/new": { "permissions": ["prompt_template:create:write", "prompt_template:detail:read"] } } ``` **前端聚合后**: ```json { "/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:按资源模块返回权限 ⭐⭐⭐⭐⭐ (最佳长期方案) #### 设计思路 **核心理念**:权限应该是**按资源模块组织**,而不是按路由组织。 #### 后端返回格式(推荐) ```json { "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`)可以是父路由权限的子集 #### 数据库设计建议 **当前表结构问题**: ```sql -- 当前:权限和路由是多对多关系 route_permissions (route_id, permission_id) ``` **推荐表结构**: ```sql -- 推荐:权限和模块是多对多关系 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[] -- 聚合后的权限列表 ) ``` #### 后端查询示例 ```sql -- 获取用户在指定模块的所有权限 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; ``` #### 前端使用方式 ```typescript // 在任何提示词相关页面,都使用同一套权限 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 - 前端聚合 **理由**: - ✅ 已经实现,默认开启 - ✅ 无需后端改动,立即解决问题 - ✅ 可以通过参数控制聚合行为 **使用方式**: ```typescript // 已默认开启,无需任何修改 // 访问 /prompts 时,会自动包含 /prompts/new 的权限 ``` **验证效果**: ```typescript // 在 /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. 权限粒度原则 **不推荐** ❌: ```json { "/prompts": ["prompt_template:list:read"], "/prompts/new": ["prompt_template:create:write"], "/prompts/:id/edit": ["prompt_template:update:write"] } ``` *问题*:权限分散,父路由无法显示子路由相关操作 **推荐** ✅: ```json { "/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` ```typescript // 默认开启子路由权限聚合 const permissionMapObj = permissionMapToObject( buildPermissionMap(routes) // aggregateChildren = true (默认) ); ``` ### 如何关闭聚合(不推荐) 如果需要关闭自动聚合(仅使用路由自身权限): ```typescript // 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