Files
leaudit-platform-frontend/auth_doc/路由权限系统实施记录.md
2025-12-05 00:09:32 +08:00

14 KiB
Raw Permalink Blame History

路由权限系统实施记录

📋 实施概览

实施日期: 2025-11-27 实施状态: 已完成 实施内容: 基于路由的细粒度权限管理系统


🎯 实施目标

实现以下功能:

  1. 后端为每个路由返回该用户在该路由下拥有的权限列表
  2. 前端构建权限映射表(路由路径 → 权限列表)
  3. 权限数据在整个应用中全局共享,避免重复请求
  4. 支持基于当前路由的自动权限检查
  5. 支持跨路由权限查询

已完成的修改

1. 后端接口类型更新

文件: app/api/auth/user-routes.ts

修改内容:

  1. 更新 BackendRouteInfo 接口 (第5-19行)

    export interface BackendRouteInfo {
      // ... 现有字段
      permissions?: string[];  // ✅ 新增:该路由下用户拥有的权限列表
      children?: BackendRouteInfo[];
    }
    
  2. 更新 MenuItem 接口 (第56-66行)

    export interface MenuItem {
      // ... 现有字段
      permissions?: string[];  // ✅ 新增:该菜单项的权限列表
      children?: MenuItem[];
    }
    
  3. 新增权限映射相关类型和工具函数 (第489-542行)

    • PermissionMap 类型定义
    • buildPermissionMap() - 从路由树提取权限映射表
    • permissionMapToObject() - 将Map转换为普通对象
    • objectToPermissionMap() - 从对象恢复Map
  4. 更新 getUserRoutesByRole 返回类型 (第551-561行)

    Promise<{
      success: boolean;
      data?: MenuItem[];
      permissionMap?: Record<string, string[]>;  // ✅ 新增
      error?: string;
      shouldRedirectToHome?: boolean
    }>
    
  5. 构建并返回权限映射表 (第668-682行)

    const permissionMapObj = permissionMapToObject(buildPermissionMap(routes));
    return {
      success: true,
      data: menuItems,
      permissionMap: permissionMapObj  // ✅ 返回权限映射表
    };
    
  6. 路由转换时传递权限 (第872-880行)

    const menuItem: MenuItem = {
      // ... 其他字段
      permissions: route.permissions  // ✅ 传递权限列表
    };
    

2. Root Loader 更新

文件: app/root.tsx

修改内容:

  1. 声明权限映射表变量 (第128行)

    let permissionMap: Record<string, string[]> = {};
    
  2. 保存权限映射表 (第171-175行)

    if (routesResult.permissionMap) {
      permissionMap = routesResult.permissionMap;
    }
    
  3. 返回权限映射表给客户端 (第251行)

    return Response.json({
      userRole,
      pathname,
      frontendJWT,
      isPublicPath,
      permissionMap,  // ✅ 传递权限映射表
      ENV: { }
    });
    

3. usePermission Hook 更新

文件: app/hooks/usePermission.tsx

修改内容:

  1. 导入 useLocation (第25行)

    import { useRouteLoaderData, useLocation } from "@remix-run/react";
    
  2. 更新接口定义 (第27-36行)

    interface RootLoaderData {
      permissions?: string[];
      permissionMap?: Record<string, string[]>;  // ✅ 新增
      userRole: string;
      userInfo?: { /* ... */ };
    }
    
  3. 获取当前路由权限 (第38-51行)

    const location = useLocation();
    const permissionMap = rootData?.permissionMap || {};
    const currentPath = location.pathname;
    const currentPermissions = permissionMap[currentPath] || [];
    const legacyPermissions = rootData?.permissions || [];
    
  4. 更新权限检查逻辑 (第58-81行)

    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行)

    return {
      permissions: currentPermissions,  // ✅ 返回当前路由的权限
      permissionMap,  // ✅ 返回完整的权限映射表
      // ...
      hasRoutePermission,  // ✅ 新增
      getCurrentPermissions,  // ✅ 新增
      getRoutePermissions,  // ✅ 新增
      // ...
    };
    

📖 使用示例

1. 基本用法:当前路由权限检查

// 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 (
    <div>
      {/* 根据权限显示/隐藏按钮 */}
      {canCreateTemplate && <Button>新增模板</Button>}
      {canEditTemplate && <Button>编辑</Button>}
      {canDeleteTemplate && <Button>删除</Button>}
    </div>
  );
}

2. 跨路由权限检查

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 (
    <div>
      {canAccessPrompts && (
        <Link to="/prompts">前往提示词管理</Link>
      )}
      <p>文档权限: {documentPermissions.join(', ')}</p>
    </div>
  );
}

3. 查看完整权限映射表(调试)

import { usePermission } from "~/hooks/usePermission";

export default function DebugPage() {
  const { permissionMap } = usePermission();

  return (
    <pre>
      {JSON.stringify(permissionMap, null, 2)}
    </pre>
  );
}

// 输出示例:
// {
//   "/prompts": [
//     "prompt_template:list:read",
//     "prompt_template:delete:delete"
//   ],
//   "/documents": [
//     "document:list:read",
//     "document:create:write",
//     ...
//   ]
// }

🔧 后端集成要求

API 返回格式要求

后端 /rbac/user/routes 接口需要在每个路由对象中添加 permissions 字段:

{
  "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<string, string[]>
   ↓
6. 转换为普通对象 permissionMapToObject(map)
   ↓
7. 返回给客户端 Response.json({ permissionMap })
   ↓
8. 数据存储在 Remix 的全局状态
   ↓
9. 组件使用 usePermission() hook
   - useRouteLoaderData("root") 获取 permissionMap
   - useLocation() 获取当前路径
   - 从 permissionMap[currentPath] 获取当前路由权限
   ↓
10. 权限检查:hasPermission('prompt_template:create:write')

权限映射表构建示例

输入(后端返回的路由树)

[
  {
    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: []
  }
]

输出(权限映射表)

{
  "/prompts": ["prompt_template:list:read", "prompt_template:delete:delete"],
  "/documents": ["document:list:read", "document:create:write"]
}

🔍 调试指南

1. 检查后端返回的数据

在浏览器控制台:

// 查看 root loader 返回的数据
window.__remixContext.state.loaderData.root

// 查看权限映射表
window.__remixContext.state.loaderData.root.permissionMap

2. 检查当前路由权限

在组件中:

const { getCurrentPermissions } = usePermission();

useEffect(() => {
  console.log('当前路由:', location.pathname);
  console.log('当前权限:', getCurrentPermissions());
}, [location.pathname]);

3. 检查权限检查逻辑

const { hasPermission } = usePermission();

const result = hasPermission('prompt_template:create:write');
console.log('权限检查结果:', result);

4. 开发模式日志

添加调试日志(仅在开发环境):

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: 使用父路由权限

const { getRoutePermissions } = usePermission();
const permissions = getRoutePermissions('/documents');

方案2: 增强匹配逻辑(在 usePermission 中)

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 执行频率:

// 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 备份(可选)

提高离线体验:

// app/hooks/usePermission.tsx
useEffect(() => {
  if (rootData?.permissionMap) {
    localStorage.setItem('permissionMap', JSON.stringify(rootData.permissionMap));
  }
}, [rootData?.permissionMap]);

3. 权限变更实时通知

使用 WebSocket 推送权限变更:

// 当管理员修改用户权限时
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. 基于路由的权限管理方案 - 详细设计方案
  2. 权限系统原理详解 - 技术原理说明
  3. 权限控制实施指南 - 通用RBAC实施指南

📝 变更历史

日期 版本 修改内容 修改人
2025-11-27 1.0.0 初始实施完成 Claude Code

文档版本: 1.0.0 最后更新: 2025-11-27 状态: 实施完成,待后端集成测试