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
+566
View File
@@ -0,0 +1,566 @@
# 权限控制实施指南
本文档说明如何在系统中实施基于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`