This commit is contained in:
2025-12-05 00:09:32 +08:00
parent bb3d22eabf
commit 3d1dbb3f97
214 changed files with 113060 additions and 1232 deletions
+585
View File
@@ -0,0 +1,585 @@
# 路由权限系统实施记录
## 📋 实施概览
**实施日期**: 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
**状态**: ✅ 实施完成,待后端集成测试