all in
This commit is contained in:
@@ -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`
|
||||
Reference in New Issue
Block a user