527 lines
14 KiB
Markdown
527 lines
14 KiB
Markdown
# 权限粒度设计方案对比
|
||
|
||
## 📋 问题描述
|
||
|
||
**场景**:
|
||
- 用户访问 `/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<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;
|
||
}
|
||
```
|
||
|
||
#### 使用方式
|
||
|
||
```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
|