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

527 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 权限粒度设计方案对比
## 📋 问题描述
**场景**
- 用户访问 `/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