docs: 添加权限系统增强设计文档
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
# 权限系统增强设计文档
|
||||
|
||||
**日期**: 2026-04-08
|
||||
**作者**: 张翔
|
||||
**版本**: 1.0
|
||||
**状态**: 待审查
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
当前系统已完成基础的路由权限控制,但存在以下优化空间:
|
||||
|
||||
1. **菜单硬编码** - 菜单在前端硬编码,无法根据用户角色动态显示
|
||||
2. **权限数据分散** - 角色和权限信息存储在 localStorage,缺乏统一管理
|
||||
3. **缺少按钮级权限控制** - 无法控制按钮级别的权限
|
||||
4. **缺少 API 权限检查** - 前端调用 API 前未检查权限,可能发送无效请求
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
实现完整的权限系统增强,包括:
|
||||
|
||||
1. **动态菜单渲染** - 从后端获取菜单数据,根据用户权限动态渲染
|
||||
2. **权限缓存优化** - 使用 Pinia 统一管理权限数据,localStorage 持久化
|
||||
3. **权限指令** - 提供 `v-permission` 指令实现按钮级权限控制
|
||||
4. **API 权限检查** - 前端调用 API 前检查权限,减少无效请求
|
||||
|
||||
### 1.3 范围
|
||||
|
||||
**包含:**
|
||||
- Permission Store (Pinia)
|
||||
- v-permission 指令
|
||||
- 动态菜单渲染
|
||||
- API 权限检查工具
|
||||
- 相关单元测试
|
||||
|
||||
**不包含:**
|
||||
- 后端权限系统修改(仅需新增 API)
|
||||
- 数据库权限表结构调整
|
||||
- 其他业务功能开发
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端应用 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 路由守卫 │ │ 权限指令 │ │ 动态菜单 │ │
|
||||
│ │ (已完成) │ │ v-permission │ │ 渲染 │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────────┼──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Permission │ │
|
||||
│ │ Store (Pinia) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ localStorage │ │
|
||||
│ │ (持久化) │ │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
HTTP API │
|
||||
│
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ 后端服务 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ /auth/login │ │ /menus/user │ │ /permissions │ │
|
||||
│ │ (已存在) │ │ (新增) │ │ (已存在) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ RBAC 权限系统 (角色-权限-菜单) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 数据流
|
||||
|
||||
```
|
||||
登录成功
|
||||
↓
|
||||
解析 JWT token 获取角色
|
||||
↓
|
||||
调用 fetchUserMenus() 获取菜单和权限
|
||||
↓
|
||||
存入 Store + localStorage
|
||||
↓
|
||||
页面刷新时从 localStorage 恢复
|
||||
```
|
||||
|
||||
## 3. 详细设计
|
||||
|
||||
### 3.1 Permission Store
|
||||
|
||||
**文件位置**: `src/stores/permission.ts`
|
||||
|
||||
**状态定义**:
|
||||
|
||||
```typescript
|
||||
interface PermissionState {
|
||||
roles: string[] // 用户角色
|
||||
permissions: string[] // 用户权限码
|
||||
menus: MenuItem[] // 用户菜单
|
||||
loaded: boolean // 是否已加载
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
icon?: string
|
||||
parentId?: number
|
||||
sort: number
|
||||
children?: MenuItem[]
|
||||
}
|
||||
```
|
||||
|
||||
**核心 Actions**:
|
||||
|
||||
```typescript
|
||||
// 初始化权限数据(从 localStorage 恢复)
|
||||
initFromStorage(): void
|
||||
|
||||
// 登录后设置权限数据
|
||||
setPermissionData(data: {
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
menus: MenuItem[]
|
||||
}): void
|
||||
|
||||
// 从后端刷新权限数据
|
||||
async fetchUserMenus(): Promise<void>
|
||||
|
||||
// 清除权限数据(退出登录)
|
||||
clearPermissionData(): void
|
||||
|
||||
// 权限检查方法
|
||||
hasRole(role: string | string[]): boolean
|
||||
hasPermission(permission: string | string[]): boolean
|
||||
```
|
||||
|
||||
**持久化策略**:
|
||||
|
||||
- 登录时:将角色、权限、菜单数据存入 localStorage
|
||||
- 页面刷新:Pinia 从 localStorage 恢复数据,立即渲染菜单
|
||||
- 权限变更:提供刷新机制,同步更新 localStorage 和 Pinia
|
||||
- 退出登录:清除所有数据
|
||||
|
||||
### 3.2 v-permission 指令
|
||||
|
||||
**文件位置**: `src/directives/permission.ts`
|
||||
|
||||
**用法**:
|
||||
|
||||
```vue
|
||||
<!-- 角色检查 -->
|
||||
<button v-permission:role="'admin'">管理员按钮</button>
|
||||
|
||||
<!-- 权限码检查 -->
|
||||
<button v-permission:permission="'user:delete'">删除用户</button>
|
||||
|
||||
<!-- 支持数组(满足任一条件) -->
|
||||
<button v-permission:role="['admin', 'manager']">导出数据</button>
|
||||
<button v-permission:permission="['user:create', 'user:update']">编辑用户</button>
|
||||
|
||||
<!-- 简写形式(默认权限检查) -->
|
||||
<button v-permission="'user:delete'">删除</button>
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
|
||||
```typescript
|
||||
export const permissionDirective = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const { arg, value } = binding
|
||||
const checkType = arg || 'permission' // 默认权限检查
|
||||
|
||||
let hasAccess = false
|
||||
|
||||
if (checkType === 'role') {
|
||||
hasAccess = permissionStore.hasRole(value)
|
||||
} else if (checkType === 'permission') {
|
||||
hasAccess = permissionStore.hasPermission(value)
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注册方式**:
|
||||
|
||||
```typescript
|
||||
// src/main.ts
|
||||
import { permissionDirective } from '@/directives/permission'
|
||||
|
||||
app.directive('permission', permissionDirective)
|
||||
```
|
||||
|
||||
### 3.3 动态菜单渲染
|
||||
|
||||
**后端 API**:
|
||||
|
||||
```
|
||||
GET /api/menus/user
|
||||
|
||||
请求头:
|
||||
Authorization: Bearer <token>
|
||||
|
||||
响应:
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"menus": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "仪表盘",
|
||||
"path": "/dashboard",
|
||||
"icon": "Odometer",
|
||||
"parentId": null,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "系统管理",
|
||||
"path": "/system",
|
||||
"icon": "Setting",
|
||||
"parentId": null,
|
||||
"sort": 2,
|
||||
"children": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "用户管理",
|
||||
"path": "/users",
|
||||
"icon": null,
|
||||
"parentId": 2,
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"user:read",
|
||||
"user:create",
|
||||
"user:update",
|
||||
"user:delete"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**前端组件**:
|
||||
|
||||
```vue
|
||||
<!-- src/layouts/DefaultLayout.vue -->
|
||||
<template>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="menu"
|
||||
:collapse="collapsed"
|
||||
router
|
||||
>
|
||||
<menu-item
|
||||
v-for="menu in menuTree"
|
||||
:key="menu.id"
|
||||
:menu="menu"
|
||||
/>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import MenuItem from '@/components/MenuItem.vue'
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
const menuTree = computed(() => permissionStore.menus)
|
||||
</script>
|
||||
```
|
||||
|
||||
**递归菜单组件**:
|
||||
|
||||
```vue
|
||||
<!-- src/components/MenuItem.vue -->
|
||||
<template>
|
||||
<!-- 有子菜单 -->
|
||||
<el-sub-menu
|
||||
v-if="menu.children && menu.children.length > 0"
|
||||
:index="String(menu.id)"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon v-if="menu.icon">
|
||||
<component :is="menu.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.id"
|
||||
:menu="child"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 无子菜单 -->
|
||||
<el-menu-item
|
||||
v-else
|
||||
:index="menu.path"
|
||||
>
|
||||
<el-icon v-if="menu.icon">
|
||||
<component :is="menu.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3.4 API 权限检查
|
||||
|
||||
**文件位置**: `src/utils/permission-check.ts`
|
||||
|
||||
**权限映射配置**:
|
||||
|
||||
```typescript
|
||||
const apiPermissionMap: Record<string, { permission: string; method: string }> = {
|
||||
'/api/users:GET': { permission: 'user:read', method: 'GET' },
|
||||
'/api/users:POST': { permission: 'user:create', method: 'POST' },
|
||||
'/api/users/*:PUT': { permission: 'user:update', method: 'PUT' },
|
||||
'/api/users/*:DELETE': { permission: 'user:delete', method: 'DELETE' },
|
||||
'/api/roles:GET': { permission: 'role:read', method: 'GET' },
|
||||
// ... 更多映射
|
||||
}
|
||||
```
|
||||
|
||||
**检查函数**:
|
||||
|
||||
```typescript
|
||||
export function canAccessApi(path: string, method: string): boolean {
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const required = findRequiredPermission(path, method, apiPermissionMap)
|
||||
|
||||
if (!required) {
|
||||
return true // 未定义权限要求的 API 默认允许
|
||||
}
|
||||
|
||||
return permissionStore.hasPermission(required.permission)
|
||||
}
|
||||
```
|
||||
|
||||
**集成到请求拦截器**:
|
||||
|
||||
```typescript
|
||||
// src/utils/request.ts
|
||||
import { canAccessApi } from './permission-check'
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
// 权限检查
|
||||
const path = config.url || ''
|
||||
const method = config.method?.toUpperCase() || 'GET'
|
||||
|
||||
if (!canAccessApi(path, method)) {
|
||||
return Promise.reject(new Error('无权限访问此 API'))
|
||||
}
|
||||
|
||||
// 原有的 token 和签名逻辑
|
||||
// ...
|
||||
|
||||
return config
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 4. 测试策略
|
||||
|
||||
### 4.1 测试覆盖范围
|
||||
|
||||
1. **Permission Store 单元测试**
|
||||
- 测试权限数据的存储和恢复
|
||||
- 测试 hasRole 和 hasPermission 方法
|
||||
- 测试 localStorage 持久化
|
||||
- 测试数据清除功能
|
||||
|
||||
2. **v-permission 指令测试**
|
||||
- 测试角色检查功能
|
||||
- 测试权限码检查功能
|
||||
- 测试数组参数处理
|
||||
- 测试元素隐藏/显示逻辑
|
||||
|
||||
3. **动态菜单测试**
|
||||
- 测试菜单数据获取
|
||||
- 测试菜单树渲染
|
||||
- 测试菜单缓存机制
|
||||
- 测试菜单权限过滤
|
||||
|
||||
4. **API 权限检查测试**
|
||||
- 测试权限映射匹配
|
||||
- 测试通配符匹配
|
||||
- 测试请求拦截逻辑
|
||||
|
||||
### 4.2 测试文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── stores/
|
||||
│ └── __tests__/
|
||||
│ └── permission.test.ts
|
||||
├── directives/
|
||||
│ └── __tests__/
|
||||
│ └── permission.test.ts
|
||||
├── components/
|
||||
│ └── __tests__/
|
||||
│ └── MenuItem.test.ts
|
||||
└── utils/
|
||||
└── __tests__/
|
||||
└── permission-check.test.ts
|
||||
```
|
||||
|
||||
## 5. 实施计划
|
||||
|
||||
### 5.1 实施顺序
|
||||
|
||||
**第 1 步:Permission Store(1-2 小时)**
|
||||
- 创建 `src/stores/permission.ts`
|
||||
- 实现 localStorage 持久化
|
||||
- 编写单元测试
|
||||
- 集成到登录流程
|
||||
|
||||
**第 2 步:v-permission 指令(1-2 小时)**
|
||||
- 创建 `src/directives/permission.ts`
|
||||
- 注册全局指令
|
||||
- 编写单元测试
|
||||
- 在现有页面应用示例
|
||||
|
||||
**第 3 步:后端 API 开发(2-3 小时)**
|
||||
- 新增 `GET /api/menus/user` 接口
|
||||
- 根据用户角色返回菜单树
|
||||
- 返回用户权限列表
|
||||
- 编写后端测试
|
||||
|
||||
**第 4 步:动态菜单渲染(2-3 小时)**
|
||||
- 创建 `src/components/MenuItem.vue`
|
||||
- 修改 `DefaultLayout.vue`
|
||||
- 集成 Permission Store
|
||||
- 编写组件测试
|
||||
|
||||
**第 5 步:API 权限检查(1-2 小时)**
|
||||
- 创建 `src/utils/permission-check.ts`
|
||||
- 集成到请求拦截器
|
||||
- 编写单元测试
|
||||
- 优化性能
|
||||
|
||||
### 5.2 后端 API 需求
|
||||
|
||||
**接口**: `GET /api/menus/user`
|
||||
|
||||
**功能**: 获取当前登录用户可访问的菜单和权限
|
||||
|
||||
**业务逻辑**:
|
||||
1. 从 token 获取用户 ID
|
||||
2. 查询用户角色
|
||||
3. 根据角色查询菜单和权限
|
||||
4. 构建菜单树结构
|
||||
5. 返回菜单和权限列表
|
||||
|
||||
**预估时间**: 7-12 小时
|
||||
|
||||
## 6. 风险和约束
|
||||
|
||||
### 6.1 技术风险
|
||||
|
||||
1. **后端 API 开发时间** - 需要后端配合开发新 API
|
||||
2. **菜单数据迁移** - 需要将硬编码菜单迁移到数据库
|
||||
3. **权限数据同步** - 前后端权限数据需要保持一致
|
||||
|
||||
### 6.2 约束条件
|
||||
|
||||
1. **向后兼容** - 需要兼容现有的路由守卫逻辑
|
||||
2. **性能要求** - 菜单加载不能影响页面首屏渲染速度
|
||||
3. **测试覆盖** - 所有新增代码需要单元测试覆盖
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
### 7.1 功能验收
|
||||
|
||||
- [ ] Permission Store 正确管理权限数据
|
||||
- [ ] v-permission 指令正确控制按钮显示
|
||||
- [ ] 动态菜单根据用户权限正确渲染
|
||||
- [ ] API 权限检查正确拦截无权限请求
|
||||
|
||||
### 7.2 质量验收
|
||||
|
||||
- [ ] 所有单元测试通过
|
||||
- [ ] 代码覆盖率 ≥ 80%
|
||||
- [ ] TypeScript 类型检查通过
|
||||
- [ ] ESLint 检查通过
|
||||
|
||||
### 7.3 性能验收
|
||||
|
||||
- [ ] 菜单加载时间 < 500ms
|
||||
- [ ] localStorage 读写不影响页面性能
|
||||
- [ ] 权限检查不影响 API 请求速度
|
||||
|
||||
## 8. 后续优化
|
||||
|
||||
### 8.1 短期优化
|
||||
|
||||
1. **权限缓存过期** - 添加权限数据过期机制
|
||||
2. **权限变更通知** - 实现权限变更后的实时通知
|
||||
3. **权限日志** - 记录权限检查日志,便于调试
|
||||
|
||||
### 8.2 长期优化
|
||||
|
||||
1. **权限可视化配置** - 提供权限配置界面
|
||||
2. **权限审计** - 记录用户权限变更历史
|
||||
3. **权限模板** - 提供常用权限模板,简化配置
|
||||
|
||||
## 9. 参考资料
|
||||
|
||||
- [Vue 3 官方文档](https://vuejs.org/)
|
||||
- [Pinia 官方文档](https://pinia.vuejs.org/)
|
||||
- [Element Plus 文档](https://element-plus.org/)
|
||||
- [RBAC 权限模型](https://en.wikipedia.org/wiki/Role-based_access_control)
|
||||
Reference in New Issue
Block a user