Files
leaudit-platform-frontend/auth_doc/user_permissions_api_design.md
T
2025-12-05 00:09:32 +08:00

273 lines
7.2 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.
# 用户权限API设计方案
## 1. 后端需要返回的数据结构
### 方案A:返回权限键数组(推荐)
**接口**: `GET /api/v3/users/{user_id}/permissions` 或集成在登录响应中
**响应示例**:
```json
{
"code": 0,
"msg": "成功",
"data": {
"user_id": 123,
"role_id": 5,
"role_key": "provincial_admin",
"role_name": "省级管理员",
"permissions": [
"prompt_template:list:read",
"prompt_template:detail:read",
"prompt_template:create:write",
"prompt_template:update:write",
"prompt_template:delete:delete",
"document:list:read",
"document:create:write"
]
}
}
```
### 方案B:返回权限对象数组(更详细)
```json
{
"code": 0,
"msg": "成功",
"data": {
"user_id": 123,
"role_id": 5,
"role_key": "provincial_admin",
"role_name": "省级管理员",
"permissions": [
{
"permission_key": "prompt_template:create:write",
"module": "prompt_template",
"resource": "create",
"action": "write",
"display_name": "创建提示词模板"
},
{
"permission_key": "prompt_template:update:write",
"module": "prompt_template",
"resource": "update",
"action": "write",
"display_name": "更新提示词模板"
}
]
}
}
```
## 2. 后端SQL查询示例
```sql
-- 获取用户的所有权限(通过角色关联)
SELECT DISTINCT
p.permission_key,
p.module,
p.resource,
p.action,
p.display_name
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN role_permissions rp ON ur.role_id = rp.role_id AND rp.grant_type = 'GRANT'
JOIN permissions p ON rp.permission_id = p.id
WHERE u.id = $1
AND u.deleted_at IS NULL
AND ur.deleted_at IS NULL;
```
## 3. 集成到现有认证流程
### 选项1:集成到JWT Token中
- 优点:前端无需额外请求,直接从JWT解析
- 缺点:JWT体积变大,权限变更需要重新登录
- 适用场景:权限不经常变化,用户会话较短
### 选项2:集成到Session中
- 优点:权限可以实时更新(每次loader都会调用getUserSession
- 缺点:每次页面加载都要查询数据库
- 适用场景:权限经常变化,需要实时控制
### 选项3:混合方案(推荐)
- JWT中存储基础权限(permission_key数组)
- 重要操作时后端再次验证
- 权限变更后强制用户重新登录或刷新token
## 4. 前端权限检查工具
### 创建权限Hook
```typescript
// app/hooks/usePermission.ts
import { useRouteLoaderData } from "@remix-run/react";
export function usePermission() {
const rootData = useRouteLoaderData("root") as {
permissions?: string[];
userRole: string;
};
const permissions = rootData?.permissions || [];
/**
* 检查是否有指定权限
* @param permissionKey 权限键,如 "prompt_template:create:write"
* @returns boolean
*/
const hasPermission = (permissionKey: string): boolean => {
return permissions.includes(permissionKey);
};
/**
* 检查是否有指定模块的任意权限
* @param module 模块名,如 "prompt_template"
* @returns boolean
*/
const hasModulePermission = (module: string): boolean => {
return permissions.some(p => p.startsWith(`${module}:`));
};
/**
* 检查是否有指定动作的权限
* @param module 模块名
* @param action 动作,如 "create", "update", "delete"
* @returns boolean
*/
const hasActionPermission = (module: string, action: string): boolean => {
return permissions.some(p =>
p.startsWith(`${module}:`) && p.includes(`:${action}`)
);
};
/**
* 批量检查权限(需要全部满足)
* @param permissionKeys 权限键数组
* @returns boolean
*/
const hasAllPermissions = (permissionKeys: string[]): boolean => {
return permissionKeys.every(key => permissions.includes(key));
};
/**
* 批量检查权限(满足任意一个即可)
* @param permissionKeys 权限键数组
* @returns boolean
*/
const hasAnyPermission = (permissionKeys: string[]): boolean => {
return permissionKeys.some(key => permissions.includes(key));
};
return {
permissions,
hasPermission,
hasModulePermission,
hasActionPermission,
hasAllPermissions,
hasAnyPermission
};
}
```
### 在组件中使用
```typescript
// app/routes/prompts._index.tsx
import { usePermission } from "~/hooks/usePermission";
export default function PromptsIndex() {
const { hasPermission, hasActionPermission } = usePermission();
// 检查是否有创建权限
const canCreate = hasPermission("prompt_template:create:write");
// 检查是否有编辑权限
const canEdit = hasPermission("prompt_template:update:write");
// 检查是否有删除权限
const canDelete = hasPermission("prompt_template:delete:delete");
// 或者使用通用检查
const canManage = hasActionPermission("prompt_template", "write");
return (
<div>
{canCreate && (
<Button onClick={() => navigate("/prompts/new")}>
</Button>
)}
{/* 表格操作列 */}
{canEdit && <button onClick={handleEdit}></button>}
{canDelete && <button onClick={handleDelete}></button>}
</div>
);
}
```
## 5. 后端验证(双重保险)
前端权限检查只是为了改善用户体验,**后端必须再次验证权限**:
```typescript
// 后端API示例(Node.js/Express风格)
app.post('/api/v3/prompt-templates', authenticate, async (req, res) => {
const userId = req.user.id;
// 验证用户是否有创建权限
const hasPermission = await checkUserPermission(
userId,
'prompt_template:create:write'
);
if (!hasPermission) {
return res.status(403).json({
code: 403,
msg: '权限不足:您没有创建提示词模板的权限'
});
}
// 执行创建逻辑...
});
```
## 6. 实施步骤
### 第一步:数据库准备
```sql
-- 确保用户角色关联表存在
-- user_roles: user_id <-> role_id
-- 确保角色权限关联表存在
-- role_permissions: role_id <-> permission_id (已存在)
-- 为现有用户分配角色和权限
```
### 第二步:后端API开发
1. 创建 `GET /api/v3/users/current/permissions` 接口
2. 在登录响应中包含权限列表
3. 在JWT payload中包含permissions字段(可选)
### 第三步:前端集成
1. 修改 `app/api/login/auth.server.ts`,在getUserSession中获取权限
2. 修改 `app/root.tsx` loader,将permissions传递给前端
3. 创建 `app/hooks/usePermission.ts`
4. 修改 `app/routes/prompts._index.tsx` 使用权限检查
### 第四步:测试验证
1. 测试不同角色的用户看到的功能是否正确
2. 测试后端API权限验证是否生效
3. 测试权限变更后是否需要重新登录
## 7. 注意事项
1. **安全性**:前端权限检查只是UI控制,不能替代后端验证
2. **性能**:权限列表应该缓存,避免每次请求都查询数据库
3. **同步性**:权限变更后,考虑强制用户重新登录或刷新token
4. **降级方案**:如果获取权限失败,应该有合理的降级策略(如只读模式)
5. **审计日志**:所有权限相关的操作应该记录审计日志