all in
This commit is contained in:
@@ -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
|
||||
**状态**: ✅ 实施完成,待后端集成测试
|
||||
Reference in New Issue
Block a user