Files
leaudit-platform-frontend/auth_doc/权限控制实施指南.md
T
2025-12-05 00:09:32 +08:00

567 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.
# 权限控制实施指南
本文档说明如何在系统中实施基于RBAC的细粒度权限控制。
## 📋 目录
1. [当前问题](#当前问题)
2. [解决方案](#解决方案)
3. [实施步骤](#实施步骤)
4. [前端使用示例](#前端使用示例)
5. [后端验证](#后端验证)
6. [测试验证](#测试验证)
---
## 当前问题
### 现有权限检查方式(硬编码)
```typescript
// ❌ 不推荐:硬编码角色判断
const hasEditPermission = userRole.toLowerCase().includes('provin');
{hasEditPermission && (
<Button onClick={handleCreate}></Button>
)}
```
**问题**
1. 无法利用数据库中的权限表(permissions、role_permissions
2. 角色和权限耦合严重,扩展性差
3. 无法实现细粒度的权限控制(如只允许编辑不允许删除)
4. 权限变更需要修改代码
---
## 解决方案
### 权限数据结构
数据库中已经有完善的权限表结构:
```sql
-- permissions 表
permissions (
id,
permission_key, -- 格式: module:resource:action
module, -- 模块名,如 prompt_template
resource, -- 资源名,如 create, list, detail
action, -- 动作,如 read, write, delete
display_name, -- 显示名称
...
)
-- role_permissions 表
role_permissions (
role_id,
permission_id,
grant_type, -- GRANT/DENY
...
)
```
### 提示词模板相关权限
```
prompt_template:list:read - 查看提示词模板列表
prompt_template:detail:read - 查看提示词模板详情
prompt_template:create:write - 创建提示词模板
prompt_template:update:write - 更新提示词模板
prompt_template:delete:delete - 删除提示词模板
```
---
## 实施步骤
### 第一步:后端返回权限列表
#### 方案A:在JWT中包含权限(推荐)
修改 `app/api/jwt-helper.server.ts`,在生成JWT时包含用户权限:
```typescript
// 查询用户权限
async function getUserPermissions(userId: number): Promise<string[]> {
const result = await apiRequest<ApiResponse<{ permissions: string[] }>>(
`/api/v3/users/${userId}/permissions`,
{ method: 'GET' }
);
return result.data?.permissions || [];
}
// 在generateJWT中添加权限
export async function generateJWT(userInfo: UserInfoForJWT): Promise<string> {
// 获取用户权限
const permissions = await getUserPermissions(userInfo.user_id);
const payload = {
...userInfo,
permissions, // ✅ 添加权限列表
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (6 * 60 * 60)
};
return jwt.sign(payload, JWT_SECRET);
}
```
#### 方案B:在后端创建权限查询API
```typescript
// app/routes/api.users.permissions.ts
export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
// 从数据库查询用户权限
const permissions = await getUserPermissionsFromDB(userInfo.user_id);
return Response.json({
code: 0,
msg: "成功",
data: { permissions }
});
}
```
#### 推荐的SQL查询
```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
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
ORDER BY p.permission_key;
```
### 第二步:前端接收和存储权限
#### 修改 `app/root.tsx` 的 loader
```typescript
export async function loader({ request }: LoaderFunctionArgs) {
try {
const session = await getUserSession(request);
const { frontendJWT, userInfo } = session;
// 解析JWT获取权限(如果JWT中包含了permissions
let permissions: string[] = [];
if (frontendJWT) {
try {
const decoded = JWTUtils.verifyJWT(frontendJWT);
permissions = decoded.permissions || [];
} catch (error) {
console.error("JWT解析失败:", error);
}
}
return Response.json({
userRole: session.userRole,
userInfo: session.userInfo,
permissions, // ✅ 传递权限列表给前端
frontendJWT,
ENV: {
API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
// ... 其他环境变量
}
});
} catch (error) {
// ... 错误处理
}
}
```
### 第三步:前端使用权限Hook
已经创建了 `app/hooks/usePermission.ts`,提供以下功能:
- `hasPermission(key)` - 检查单个权限
- `canCreate(module)` - 检查创建权限
- `canUpdate(module)` - 检查更新权限
- `canDelete(module)` - 检查删除权限
- `canView(module)` - 检查查看权限
- `PermissionGuard` - 权限包装组件
---
## 前端使用示例
### 示例1:控制按钮显示(推荐方式)
```typescript
import { PermissionGuard } from "~/hooks/usePermission";
export default function PromptsIndex() {
return (
<div>
{/* ✅ 使用PermissionGuard包装需要权限控制的组件 */}
<PermissionGuard permission="prompt_template:create:write">
<Button onClick={handleCreate}></Button>
</PermissionGuard>
</div>
);
}
```
### 示例2:使用Hook进行逻辑判断
```typescript
import { usePermission } from "~/hooks/usePermission";
export default function PromptsIndex() {
const { canCreate, canUpdate, canDelete } = usePermission();
const canCreateTemplate = canCreate('prompt_template');
const canEditTemplate = canUpdate('prompt_template');
const canDeleteTemplate = canDelete('prompt_template');
return (
<div>
{canCreateTemplate && (
<Button onClick={handleCreate}></Button>
)}
{canEditTemplate ? (
<Button onClick={handleEdit}></Button>
) : (
<Button onClick={handleView}></Button>
)}
{canDeleteTemplate && (
<Button onClick={handleDelete}></Button>
)}
</div>
);
}
```
### 示例3:表格操作列权限控制
```typescript
const columns = [
// ... 其他列
{
title: "操作",
key: "operation",
render: (_: unknown, record: PromptTemplateUI) => (
<div>
{record.status === 'system' ? (
// 系统预设模板
<>
<button onClick={() => handleView(record.id)}></button>
{canCreateTemplate && (
<button onClick={() => handleClone(record.id)}></button>
)}
</>
) : (
// 自定义模板
<>
{canEditTemplate ? (
<button onClick={() => handleEdit(record.id)}></button>
) : canViewTemplate ? (
<button onClick={() => handleView(record.id)}></button>
) : null}
{canDeleteTemplate && (
<button onClick={() => handleDelete(record.id)}></button>
)}
</>
)}
</div>
)
}
];
```
### 示例4:多权限检查
```typescript
import { usePermission } from "~/hooks/usePermission";
export default function MyComponent() {
const { hasAllPermissions, hasAnyPermission } = usePermission();
// 需要同时拥有多个权限
const canManageTemplates = hasAllPermissions([
'prompt_template:create:write',
'prompt_template:update:write',
'prompt_template:delete:delete'
]);
// 只需拥有其中一个权限
const canAccessTemplates = hasAnyPermission([
'prompt_template:list:read',
'prompt_template:detail:read'
]);
return (
<div>
{canManageTemplates && <AdminPanel />}
{canAccessTemplates && <TemplateList />}
</div>
);
}
```
---
## 后端验证
**重要**:前端权限检查只是UI控制,后端必须再次验证权限!
### Express/Koa 中间件示例
```typescript
// 权限验证中间件
async function requirePermission(permissionKey: string) {
return async (req, res, next) => {
const userId = req.user.id;
// 查询用户是否有该权限
const hasPermission = await checkUserPermission(userId, permissionKey);
if (!hasPermission) {
return res.status(403).json({
code: 403,
msg: `权限不足:您没有${permissionKey}的权限`
});
}
next();
};
}
// 使用示例
app.post(
'/api/v3/prompt-templates',
authenticate,
requirePermission('prompt_template:create:write'),
async (req, res) => {
// 执行创建逻辑
}
);
```
### Remix Action 权限验证
```typescript
// app/routes/prompts._index.tsx
export async function action({ request }: ActionFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request);
const formData = await request.formData();
const intent = formData.get("intent") as string;
if (intent === "delete") {
// ✅ 后端再次验证权限
const permissions = await getUserPermissions(userInfo.user_id);
const hasDeletePermission = permissions.includes('prompt_template:delete:delete');
if (!hasDeletePermission) {
return Response.json({
success: false,
error: '权限不足:您没有删除提示词模板的权限'
}, { status: 403 });
}
// 执行删除逻辑
const result = await deletePromptTemplate(id, frontendJWT);
return Response.json({ success: true });
}
}
```
---
## 测试验证
### 1. 准备测试数据
```sql
-- 创建测试角色
INSERT INTO roles (role_key, role_name, description)
VALUES
('viewer', '查看者', '只能查看数据'),
('editor', '编辑者', '可以查看和编辑数据'),
('admin', '管理员', '拥有所有权限');
-- 分配权限给角色
-- 查看者:只有查看权限
INSERT INTO role_permissions (role_id, permission_id, grant_type)
SELECT 1, id, 'GRANT'
FROM permissions
WHERE permission_key IN (
'prompt_template:list:read',
'prompt_template:detail:read'
);
-- 编辑者:有查看、创建、更新权限
INSERT INTO role_permissions (role_id, permission_id, grant_type)
SELECT 2, id, 'GRANT'
FROM permissions
WHERE permission_key IN (
'prompt_template:list:read',
'prompt_template:detail:read',
'prompt_template:create:write',
'prompt_template:update:write'
);
-- 管理员:所有权限
INSERT INTO role_permissions (role_id, permission_id, grant_type)
SELECT 3, id, 'GRANT'
FROM permissions
WHERE module = 'prompt_template';
-- 分配角色给用户
INSERT INTO user_roles (user_id, role_id)
VALUES
(1, 1), -- 用户1是查看者
(2, 2), -- 用户2是编辑者
(3, 3); -- 用户3是管理员
```
### 2. 测试用例
| 用户角色 | 查看列表 | 查看详情 | 新增按钮 | 编辑按钮 | 删除按钮 | 复制按钮 |
|---------|---------|---------|---------|---------|---------|---------|
| 查看者 | ✅ 显示 | ✅ 显示 | ❌ 隐藏 | ❌ 隐藏(显示查看)| ❌ 隐藏 | ❌ 隐藏 |
| 编辑者 | ✅ 显示 | ✅ 显示 | ✅ 显示 | ✅ 显示 | ❌ 隐藏 | ✅ 显示 |
| 管理员 | ✅ 显示 | ✅ 显示 | ✅ 显示 | ✅ 显示 | ✅ 显示 | ✅ 显示 |
### 3. 测试步骤
1. **清空浏览器缓存**(清除localStorage和Cookie
2. **使用不同角色的用户登录**
3. **检查页面元素**
- 打开浏览器开发者工具 → Console
- 查看日志输出:
```
📋 [Prompts] 权限列表: ["prompt_template:list:read", ...]
📋 [Prompts] 权限检查结果: {canCreate: true, canEdit: true, ...}
```
4. **尝试操作**
- 点击新增按钮(如果可见)
- 点击编辑按钮
- 尝试删除操作
5. **检查后端验证**
- 使用Postman或curl直接调用API
- 使用不具备权限的用户token
- 应该返回403错误
```bash
# 测试删除API(使用查看者的token)
curl -X POST http://localhost:5173/prompts \
-H "Authorization: Bearer {viewer_token}" \
-d "intent=delete&id=123"
# 预期响应:
{
"success": false,
"error": "权限不足:您没有删除提示词模板的权限"
}
```
---
## 降级方案
如果后端暂时无法返回权限列表,`usePermission` Hook 有降级逻辑:
```typescript
// 降级方案:使用角色判断
if (permissions.length === 0) {
// 如果角色包含'provin',给予所有权限
if (userRole.toLowerCase().includes('provin')) {
return true;
}
// 默认只有查看权限
if (permissionKey.includes(':read')) {
return true;
}
return false;
}
```
这样即使后端还没有实现权限API,前端代码也能正常工作(使用旧的角色判断逻辑)。
---
## 常见问题
### Q1: 权限变更后需要重新登录吗?
**A**: 取决于实现方式:
- 如果权限存储在JWT中:需要重新登录(或刷新token)
- 如果每次请求都查询数据库:立即生效
**推荐**:使用JWT存储+定期刷新的方式,在关键操作时重新验证。
### Q2: 如何处理权限不足的提示?
**A**:
- 前端:隐藏按钮 > 禁用按钮 > 点击后提示
- 后端:返回403状态码 + 友好的错误信息
- 用户体验:使用toastService显示错误提示
### Q3: 如何调试权限问题?
**A**:
1. 检查浏览器Console,查看权限列表和检查结果
2. 使用开发者工具检查JWT payload
3. 在数据库中查询用户的角色和权限关联
4. 检查后端日志,确认权限验证是否执行
### Q4: 性能优化建议?
**A**:
1. 权限列表应该缓存(Redis或内存缓存)
2. JWT中包含权限可以减少数据库查询
3. 使用roles表的permissions_cache字段存储预计算的权限
4. 考虑使用WebSocket推送权限变更通知
---
## 总结
### 优势
✅ **灵活性**:权限可以在数据库中动态配置,无需修改代码
✅ **细粒度**:可以精确控制每个功能的访问权限
✅ **可扩展**:新增权限只需在数据库中添加记录
✅ **安全性**:前后端双重验证,防止越权操作
✅ **可维护**:权限逻辑集中管理,易于理解和维护
### 最佳实践
1. **前端仅做UI控制**:隐藏/禁用按钮,提升用户体验
2. **后端必须验证**:所有API都要检查权限,防止绕过前端限制
3. **使用语义化的权限键**`module:resource:action` 格式清晰易懂
4. **记录审计日志**:所有权限相关操作都应该记录
5. **定期审查权限**:定期检查用户权限配置是否合理
---
**文档更新日期**: 2025-11-27
**作者**: Claude Code
**相关文件**:
- `app/hooks/usePermission.ts`
- `app/routes/prompts._index.tsx`
- `auth_doc/user_permissions_api_design.md`