# 权限控制实施指南 本文档说明如何在系统中实施基于RBAC的细粒度权限控制。 ## 📋 目录 1. [当前问题](#当前问题) 2. [解决方案](#解决方案) 3. [实施步骤](#实施步骤) 4. [前端使用示例](#前端使用示例) 5. [后端验证](#后端验证) 6. [测试验证](#测试验证) --- ## 当前问题 ### 现有权限检查方式(硬编码) ```typescript // ❌ 不推荐:硬编码角色判断 const hasEditPermission = userRole.toLowerCase().includes('provin'); {hasEditPermission && ( )} ``` **问题**: 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 { const result = await apiRequest>( `/api/v3/users/${userId}/permissions`, { method: 'GET' } ); return result.data?.permissions || []; } // 在generateJWT中添加权限 export async function generateJWT(userInfo: UserInfoForJWT): Promise { // 获取用户权限 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 (
{/* ✅ 使用PermissionGuard包装需要权限控制的组件 */}
); } ``` ### 示例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 (
{canCreateTemplate && ( )} {canEditTemplate ? ( ) : ( )} {canDeleteTemplate && ( )}
); } ``` ### 示例3:表格操作列权限控制 ```typescript const columns = [ // ... 其他列 { title: "操作", key: "operation", render: (_: unknown, record: PromptTemplateUI) => (
{record.status === 'system' ? ( // 系统预设模板 <> {canCreateTemplate && ( )} ) : ( // 自定义模板 <> {canEditTemplate ? ( ) : canViewTemplate ? ( ) : null} {canDeleteTemplate && ( )} )}
) } ]; ``` ### 示例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 (
{canManageTemplates && } {canAccessTemplates && }
); } ``` --- ## 后端验证 **重要**:前端权限检查只是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`