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

586 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 路由权限系统实施记录
## 📋 实施概览
**实施日期**: 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<string, string[]>; // ✅ 新增
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<string, string[]> = {};
```
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<string, string[]>; // ✅ 新增
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 (
<div>
{/* 根据权限显示/隐藏按钮 */}
{canCreateTemplate && <Button>新增模板</Button>}
{canEditTemplate && <Button>编辑</Button>}
{canDeleteTemplate && <Button>删除</Button>}
</div>
);
}
```
### 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 (
<div>
{canAccessPrompts && (
<Link to="/prompts">前往提示词管理</Link>
)}
<p>文档权限: {documentPermissions.join(', ')}</p>
</div>
);
}
```
### 3. 查看完整权限映射表(调试)
```typescript
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` 字段:
```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<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')
```
### 权限映射表构建示例
**输入(后端返回的路由树)**
```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
**状态**: ✅ 实施完成,待后端集成测试