This commit is contained in:
2025-12-05 00:09:32 +08:00
parent bb3d22eabf
commit 3d1dbb3f97
214 changed files with 113060 additions and 1232 deletions
+526
View File
@@ -0,0 +1,526 @@
# 权限粒度设计方案对比
## 📋 问题描述
**场景**
- 用户访问 `/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