14 KiB
权限粒度设计方案对比
📋 问题描述
场景:
- 用户访问
/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"
]
}
]
}
设计原则
- 模块级权限 - 同一资源模块的所有路由共享权限池
- 父路由包含全集 - 父路由(如
/prompts)应该包含该模块的所有用户权限 - 子路由可以限制 - 子路由(如
/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:数据库改造
- 在
routes表添加module_name字段 - 创建
module_permissions视图 - 更新权限查询逻辑
阶段2:后端API调整
- 修改
/rbac/user/routes接口 - 按模块聚合权限,而非按路由
- 确保父路由包含模块的所有权限
阶段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)
- 长期:规划模块化权限设计(方案3)
注意事项
⚠️ 前端权限检查仅用于UI控制 ⚠️ 后端必须验证所有操作的权限 ⚠️ 定期审查权限配置的合理性
文档版本: 1.0.0 创建日期: 2025-11-27 作者: Claude Code