567 lines
14 KiB
Markdown
567 lines
14 KiB
Markdown
# 权限控制实施指南
|
||
|
||
本文档说明如何在系统中实施基于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`
|