# 路由权限系统实施记录 ## 📋 实施概览 **实施日期**: 2025-11-27 **实施状态**: ✅ 已完成 **实施内容**: 基于路由的细粒度权限管理系统 --- ## 🎯 实施目标 实现以下功能: 1. 后端为每个路由返回该用户在该路由下拥有的权限列表 2. 前端构建权限映射表(路由路径 → 权限列表) 3. 权限数据在整个应用中全局共享,避免重复请求 4. 支持基于当前路由的自动权限检查 5. 支持跨路由权限查询 --- ## ✅ 已完成的修改 ### 1. 后端接口类型更新 **文件**: `app/api/auth/user-routes.ts` #### 修改内容: 1. **更新 `BackendRouteInfo` 接口** (第5-19行) ```typescript export interface BackendRouteInfo { // ... 现有字段 permissions?: string[]; // ✅ 新增:该路由下用户拥有的权限列表 children?: BackendRouteInfo[]; } ``` 2. **更新 `MenuItem` 接口** (第56-66行) ```typescript export interface MenuItem { // ... 现有字段 permissions?: string[]; // ✅ 新增:该菜单项的权限列表 children?: MenuItem[]; } ``` 3. **新增权限映射相关类型和工具函数** (第489-542行) - `PermissionMap` 类型定义 - `buildPermissionMap()` - 从路由树提取权限映射表 - `permissionMapToObject()` - 将Map转换为普通对象 - `objectToPermissionMap()` - 从对象恢复Map 4. **更新 `getUserRoutesByRole` 返回类型** (第551-561行) ```typescript Promise<{ success: boolean; data?: MenuItem[]; permissionMap?: Record; // ✅ 新增 error?: string; shouldRedirectToHome?: boolean }> ``` 5. **构建并返回权限映射表** (第668-682行) ```typescript const permissionMapObj = permissionMapToObject(buildPermissionMap(routes)); return { success: true, data: menuItems, permissionMap: permissionMapObj // ✅ 返回权限映射表 }; ``` 6. **路由转换时传递权限** (第872-880行) ```typescript const menuItem: MenuItem = { // ... 其他字段 permissions: route.permissions // ✅ 传递权限列表 }; ``` --- ### 2. Root Loader 更新 **文件**: `app/root.tsx` #### 修改内容: 1. **声明权限映射表变量** (第128行) ```typescript let permissionMap: Record = {}; ``` 2. **保存权限映射表** (第171-175行) ```typescript if (routesResult.permissionMap) { permissionMap = routesResult.permissionMap; } ``` 3. **返回权限映射表给客户端** (第251行) ```typescript return Response.json({ userRole, pathname, frontendJWT, isPublicPath, permissionMap, // ✅ 传递权限映射表 ENV: { } }); ``` --- ### 3. usePermission Hook 更新 **文件**: `app/hooks/usePermission.tsx` #### 修改内容: 1. **导入 useLocation** (第25行) ```typescript import { useRouteLoaderData, useLocation } from "@remix-run/react"; ``` 2. **更新接口定义** (第27-36行) ```typescript interface RootLoaderData { permissions?: string[]; permissionMap?: Record; // ✅ 新增 userRole: string; userInfo?: { /* ... */ }; } ``` 3. **获取当前路由权限** (第38-51行) ```typescript const location = useLocation(); const permissionMap = rootData?.permissionMap || {}; const currentPath = location.pathname; const currentPermissions = permissionMap[currentPath] || []; const legacyPermissions = rootData?.permissions || []; ``` 4. **更新权限检查逻辑** (第58-81行) ```typescript const hasPermission = (permissionKey: string): boolean => { // 优先使用当前路由的权限列表 if (currentPermissions.length > 0) { return currentPermissions.includes(permissionKey); } // 向后兼容:支持旧的permissions数组 if (legacyPermissions.length > 0) { return legacyPermissions.includes(permissionKey); } // 降级方案... }; ``` 5. **新增路由权限查询方法** (第83-109行) - `hasRoutePermission(path, permissionKey)` - 检查指定路由的权限 - `getCurrentPermissions()` - 获取当前路由的所有权限 - `getRoutePermissions(path)` - 获取指定路由的所有权限 6. **更新返回对象** (第184-209行) ```typescript return { permissions: currentPermissions, // ✅ 返回当前路由的权限 permissionMap, // ✅ 返回完整的权限映射表 // ... hasRoutePermission, // ✅ 新增 getCurrentPermissions, // ✅ 新增 getRoutePermissions, // ✅ 新增 // ... }; ``` --- ## 📖 使用示例 ### 1. 基本用法:当前路由权限检查 ```typescript // app/routes/prompts._index.tsx import { usePermission } from "~/hooks/usePermission"; export default function PromptsIndex() { const { canCreate, canUpdate, canDelete, getCurrentPermissions } = usePermission(); // 调试:查看当前路由权限 useEffect(() => { console.log('当前路由权限:', getCurrentPermissions()); // 输出: ["prompt_template:list:read", "prompt_template:delete:delete"] }, []); const canCreateTemplate = canCreate('prompt_template'); const canEditTemplate = canUpdate('prompt_template'); const canDeleteTemplate = canDelete('prompt_template'); return (
{/* 根据权限显示/隐藏按钮 */} {canCreateTemplate && } {canEditTemplate && } {canDeleteTemplate && }
); } ``` ### 2. 跨路由权限检查 ```typescript import { usePermission } from "~/hooks/usePermission"; export default function Dashboard() { const { hasRoutePermission, getRoutePermissions } = usePermission(); // 检查其他路由的权限 const canAccessPrompts = hasRoutePermission( '/prompts', 'prompt_template:list:read' ); const documentPermissions = getRoutePermissions('/documents'); return (
{canAccessPrompts && ( 前往提示词管理 )}

文档权限: {documentPermissions.join(', ')}

); } ``` ### 3. 查看完整权限映射表(调试) ```typescript import { usePermission } from "~/hooks/usePermission"; export default function DebugPage() { const { permissionMap } = usePermission(); return (
      {JSON.stringify(permissionMap, null, 2)}
    
); } // 输出示例: // { // "/prompts": [ // "prompt_template:list:read", // "prompt_template:delete:delete" // ], // "/documents": [ // "document:list:read", // "document:create:write", // ... // ] // } ``` --- ## 🔧 后端集成要求 ### API 返回格式要求 后端 `/rbac/user/routes` 接口需要在每个路由对象中添加 `permissions` 字段: ```json { "code": 0, "msg": "成功", "data": { "user_id": 123, "username": "张三", "routes": [ { "id": 10, "route_path": "/prompts", "route_name": "prompts", "route_title": "提示词管理", "icon": "ri-chat-1-line", "parent_id": 5, "sort_order": 3, "is_hidden": false, "permissions": [ "prompt_template:list:read", "prompt_template:detail:read", "prompt_template:delete:delete" ] } ] } } ``` ### 权限键命名规范 格式:`module:resource:action` **示例**: - `prompt_template:list:read` - 查看提示词列表 - `prompt_template:create:write` - 创建提示词 - `prompt_template:update:write` - 更新提示词 - `prompt_template:delete:delete` - 删除提示词 - `prompt_template:detail:read` - 查看提示词详情 --- ## 📊 数据流程 ### 完整的数据流 ``` 1. 用户访问任意路由 (如 /prompts) ↓ 2. root.tsx loader 执行 ↓ 3. 调用 getUserRoutesByRole(userRole, jwt, true) ↓ 4. 后端返回路由树(包含每个路由的 permissions 字段) ↓ 5. 前端执行 buildPermissionMap(routes) - 递归遍历路由树 - 提取每个路由的 route_path 和 permissions - 构建 Map ↓ 6. 转换为普通对象 permissionMapToObject(map) ↓ 7. 返回给客户端 Response.json({ permissionMap }) ↓ 8. 数据存储在 Remix 的全局状态 ↓ 9. 组件使用 usePermission() hook - useRouteLoaderData("root") 获取 permissionMap - useLocation() 获取当前路径 - 从 permissionMap[currentPath] 获取当前路由权限 ↓ 10. 权限检查:hasPermission('prompt_template:create:write') ``` ### 权限映射表构建示例 **输入(后端返回的路由树)**: ```typescript [ { id: 1, route_path: "/prompts", permissions: ["prompt_template:list:read", "prompt_template:delete:delete"], children: [] }, { id: 2, route_path: "/documents", permissions: ["document:list:read", "document:create:write"], children: [] } ] ``` **输出(权限映射表)**: ```typescript { "/prompts": ["prompt_template:list:read", "prompt_template:delete:delete"], "/documents": ["document:list:read", "document:create:write"] } ``` --- ## 🔍 调试指南 ### 1. 检查后端返回的数据 在浏览器控制台: ```javascript // 查看 root loader 返回的数据 window.__remixContext.state.loaderData.root // 查看权限映射表 window.__remixContext.state.loaderData.root.permissionMap ``` ### 2. 检查当前路由权限 在组件中: ```typescript const { getCurrentPermissions } = usePermission(); useEffect(() => { console.log('当前路由:', location.pathname); console.log('当前权限:', getCurrentPermissions()); }, [location.pathname]); ``` ### 3. 检查权限检查逻辑 ```typescript const { hasPermission } = usePermission(); const result = hasPermission('prompt_template:create:write'); console.log('权限检查结果:', result); ``` ### 4. 开发模式日志 添加调试日志(仅在开发环境): ```typescript useEffect(() => { if (process.env.NODE_ENV === 'development') { console.group('🔐 权限调试信息'); console.log('当前路由:', location.pathname); console.log('当前权限:', getCurrentPermissions()); console.log('完整映射表:', permissionMap); console.groupEnd(); } }, [location.pathname]); ``` --- ## ⚠️ 重要注意事项 ### 1. 前端权限检查 ≠ 安全防护 - **前端权限检查仅用于UI控制**(隐藏/禁用按钮) - **真正的权限验证必须在后端进行** - 攻击者可以绕过前端检查直接调用API - 永远不要依赖前端权限检查来保护敏感数据 ### 2. 动态路由的权限处理 当前实现使用精确路径匹配 (`permissionMap[pathname]`),对于动态路由需要特殊处理: **问题场景**: ``` 当前路由: /documents/123 权限映射表: { "/documents": [...] } 结果: permissionMap["/documents/123"] 返回 undefined ``` **解决方案**: **方案1**: 使用父路由权限 ```typescript const { getRoutePermissions } = usePermission(); const permissions = getRoutePermissions('/documents'); ``` **方案2**: 增强匹配逻辑(在 usePermission 中) ```typescript const findMatchingRoute = (pathname: string): string[] => { // 精确匹配 if (permissionMap[pathname]) { return permissionMap[pathname]; } // 前缀匹配(找最长匹配) const matchingRoutes = Object.keys(permissionMap) .filter(route => pathname.startsWith(route)) .sort((a, b) => b.length - a.length); return matchingRoutes.length > 0 ? permissionMap[matchingRoutes[0]] : []; }; ``` ### 3. 性能考虑 - 权限映射表在 root loader 中加载**一次** - 在 SPA 导航过程中不会重新加载 - 内存占用极小(通常 < 10KB) - 无需额外的性能优化 ### 4. 降级策略 系统实现了多级降级,确保健壮性: 1. **优先级1**: 当前路由权限(permissionMap[currentPath]) 2. **优先级2**: 旧的 permissions 数组(向后兼容) 3. **优先级3**: 基于 userRole 的角色判断 4. **优先级4**: 默认查看权限(:read) --- ## 📈 后续优化建议 ### 1. Session 缓存(可选) 减少 root loader 执行频率: ```typescript // app/root.tsx const cachedPermissions = session.get("permissionMap"); const cacheTimestamp = session.get("permissionMapTimestamp"); const CACHE_TTL = 5 * 60 * 1000; // 5分钟 if (cachedPermissions && (Date.now() - cacheTimestamp < CACHE_TTL)) { return Response.json({ permissionMap: cachedPermissions }); } ``` ### 2. LocalStorage 备份(可选) 提高离线体验: ```typescript // app/hooks/usePermission.tsx useEffect(() => { if (rootData?.permissionMap) { localStorage.setItem('permissionMap', JSON.stringify(rootData.permissionMap)); } }, [rootData?.permissionMap]); ``` ### 3. 权限变更实时通知 使用 WebSocket 推送权限变更: ```typescript // 当管理员修改用户权限时 socket.emit('permission-changed', { userId }); // 客户端监听 socket.on('permission-changed', () => { window.location.reload(); // 重新加载权限 }); ``` --- ## ✅ 测试清单 实施完成后,请验证以下项目: - [ ] 后端 `/rbac/user/routes` 接口已添加 `permissions` 字段 - [ ] 用户登录后,root loader 成功获取权限映射表 - [ ] 在浏览器控制台可以查看 `permissionMap` 数据 - [ ] `/prompts` 页面根据权限正确显示/隐藏按钮 - [ ] `canCreate()`, `canUpdate()`, `canDelete()` 方法正常工作 - [ ] 跨路由权限检查 `hasRoutePermission()` 正常工作 - [ ] `getCurrentPermissions()` 返回正确的权限列表 - [ ] 权限变更后刷新页面可以看到更新 - [ ] 降级策略正常(后端未返回 permissions 时系统仍能运行) - [ ] TypeScript 类型检查通过,无类型错误 - [ ] 不同角色用户看到的按钮不同 - [ ] 后端 API 正确验证权限(403 错误) --- ## 📚 相关文档 1. [基于路由的权限管理方案](./基于路由的权限管理方案.md) - 详细设计方案 2. [权限系统原理详解](./权限系统原理详解.md) - 技术原理说明 3. [权限控制实施指南](./权限控制实施指南.md) - 通用RBAC实施指南 --- ## 📝 变更历史 | 日期 | 版本 | 修改内容 | 修改人 | |------|------|---------|--------| | 2025-11-27 | 1.0.0 | 初始实施完成 | Claude Code | --- **文档版本**: 1.0.0 **最后更新**: 2025-11-27 **状态**: ✅ 实施完成,待后端集成测试