🛡️ 权限使用
ZsAdmin 前端采用基于角色的权限控制(RBAC)模型,实现了细粒度的权限管理。本章节将详细介绍 ZsAdmin 前端的权限管理机制和使用方法。
1. 📋 权限管理概述
1.1 RBAC 模型
RBAC(Role-Based Access Control)是一种基于角色的权限控制模型,它将权限与角色关联,用户通过分配角色获得相应的权限。ZsAdmin 前端实现了完整的 RBAC 模型,包括:
- 用户:系统中的操作者
- 角色:权限的集合
- 权限:对资源的操作许可
- 资源:系统中的各种资源,如菜单、按钮、API 等
1.2 权限类型
ZsAdmin 前端支持以下几种权限类型:
| 权限类型 | 说明 | 示例 |
|---|---|---|
| 菜单权限 | 控制用户是否可以看到某个菜单 | 仪表盘菜单 |
| 按钮权限 | 控制用户是否可以看到某个按钮 | 新增、编辑、删除按钮 |
| API 权限 | 控制用户是否可以访问某个 API | /api/user/delete |
| 数据权限 | 控制用户可以访问的数据范围 | 只能查看自己创建的数据 |
2. ⚙️ 权限配置
2.1 权限定义
在后端系统中,管理员可以定义各种权限,包括菜单权限、按钮权限和 API 权限。权限定义通常包括:
- 权限名称
- 权限编码
- 权限类型
- 资源路径
- 描述
2.2 角色配置
管理员可以创建角色,并为角色分配相应的权限。角色配置通常包括:
- 角色名称
- 角色编码
- 角色描述
- 分配的权限列表
2.3 用户角色分配
管理员可以为用户分配一个或多个角色,用户将获得所有分配角色的权限。
3. 🔧 权限实现
3.1 权限获取
当用户登录成功后,前端会从后端获取用户的权限信息,包括:
- 用户拥有的角色列表
- 用户拥有的权限列表
这些权限信息会被存储在状态管理中,供前端应用使用。
3.2 权限存储
权限信息存储在 Pinia 状态管理中,使用 useUserStore 进行管理:
typescript
import { defineStore } from 'pinia'
interface User {
age?: number
avatar?: string
email?: string
ip?: string
ipAddress?: string
isAdmin?: boolean
lastLoginTime?: string
phone?: string
realName?: string
sex?: number
status?: number
sysDeptId?: number
sysPostId?: number
sysUserId?: number
username?: string
}
interface UserState {
user: User
permissions: string[]
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: {},
permissions: [],
}),
getters: {
// 检查是否拥有指定权限
hasPermission: (state) => (permission: string) => {
return state.permissions.includes(permission)
},
// 检查是否拥有任一权限
hasAnyPermission: (state) => (permissions: string[]) => {
return permissions.some(perm => state.permissions.includes(perm))
},
// 检查是否拥有所有权限
hasAllPermissions: (state) => (permissions: string[]) => {
return permissions.every(perm => state.permissions.includes(perm))
},
},
actions: {
// 从后端获取用户信息和权限
async info() {
const response = await getUserInfo()
this.user = response.data.user
this.permissions = response.data.permissions || []
},
// 清除用户信息
logout() {
this.user = {}
this.permissions = []
removeToken()
}
}
})3.3 权限校验
3.3.1 路由权限校验
在路由配置中,可以通过 meta.requireAuth 属性指定该路由是否需要权限验证:
typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: '仪表盘',
icon: 'dashboard',
requireAuth: true, // 需要权限验证
}
}
]
})然后在路由守卫中进行权限校验:
typescript
router.beforeEach(async (to, from, next) => {
NProgress.start();
const isLoginPage = to.path === '/login';
// 已登录的情况
if (isLogin()) {
if (isLoginPage) {
next('/dashboard');
NProgress.done();
return;
}
const userStore = useUserStore();
const appStore = useAppStore();
if (!appStore.hasFetchedMenus) {
try {
await userStore.info();
await appStore.fetchServerMenuConfig();
const newRoutes = transformRoutes(appStore.serverMenu);
addRoutesToRouter(router, newRoutes);
addSpecialRoutes(router); // 加载完动态路由后添加特殊路由
next({ ...to, replace: true });
NProgress.done();
} catch (error) {
/* eslint-disable no-console */
console.error('路由守卫加载菜单失败:', error);
await userStore.logout();
next('/login'); // 跳转到登录页
NProgress.done();
}
} else {
// 如果已经加载过动态路由,则直接放行
next();
NProgress.done();
}
} else {
if (to.meta.ignoreAuth) {
next();
} else {
next({
path: '/login',
query: { redirect: to.path, ...to.query } as LocationQueryRaw,
});
}
NProgress.done();
}
});3.3.2 组件权限校验
在组件中,可以使用 v-permission 指令来控制按钮等元素的显示与隐藏:
vue
<template>
<div>
<el-button type="primary" v-permission="'sys:user:add'">新增用户</el-button>
<el-button type="success" v-permission="['sys:user:update', 'sys:user:edit']">编辑用户</el-button>
<el-button type="danger" v-permission="'sys:user:delete'">删除用户</el-button>
</div>
</template>v-permission 指令的实现:
typescript
import { DirectiveBinding, App } from 'vue';
import { useUserStore } from '@/store';
/**
* 权限指令实现
* 支持单个权限字符串或权限数组
*/
const permission = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding);
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding);
},
};
/**
* 检查权限
*/
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (!value) return;
const userStore = useUserStore();
const { permissions } = userStore;
let hasPermission = false;
if (typeof value === 'string') {
// 单个权限字符串
hasPermission = Array.isArray(permissions) && permissions.includes(value);
} else if (Array.isArray(value)) {
// 权限数组,只要拥有任一权限即可
hasPermission = Array.isArray(permissions) && value.some(perm => permissions.includes(perm));
}
if (!hasPermission && el.parentNode) {
el.parentNode.removeChild(el);
}
}
export function setupDirectives(app: App) {
app.directive('permission', permission)
}4. 📖 权限管理示例
4.1 配置菜单权限
后端定义菜单权限:
- 权限编码:
sys:user:view - 权限名称:查看用户管理
- 权限类型:菜单权限
- 资源路径:
/system/user
- 权限编码:
前端路由配置:
typescript{ path: '/system/user', name: 'User', component: () => import('@/views/system/user/index.vue'), meta: { title: '用户管理', icon: 'user', requireAuth: true, permission: 'sys:user:view' } }管理员分配权限:
- 为角色分配
sys:user:view权限 - 为用户分配角色
- 为角色分配
4.2 配置按钮权限
后端定义按钮权限:
- 权限编码:
sys:user:add - 权限名称:新增用户
- 权限类型:按钮权限
- 资源路径:
/api/user/add
- 权限编码:
前端使用指令:
vue<el-button type="primary" v-permission="'sys:user:add'">新增用户</el-button>管理员为用户分配权限:
- 为角色分配
sys:user:add权限 - 为用户分配角色
- 为角色分配
5. 💡 最佳实践
5.1 权限设计原则
- 最小权限原则:只授予用户完成工作所需的最小权限
- 权限分层:按功能模块和操作类型进行权限分层设计
- 权限命名规范:使用
模块:功能:操作的格式命名权限,如sys:user:add - 定期权限审计:定期检查和清理用户权限,移除不必要的权限
5.2 前端实现建议
- 避免硬编码权限:权限编码应从后端获取,避免在前端代码中硬编码
- 权限缓存策略:合理缓存权限信息,减少后端请求
- 错误处理:权限校验失败时给出友好提示
- 调试模式:开发环境提供权限模拟功能,方便测试
5.3 常见问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 权限不生效 | 检查权限编码是否正确,确保用户已被分配相应权限 |
| 动态路由加载失败 | 检查路由配置和后端返回的菜单数据格式是否匹配 |
| 按钮不显示 | 检查 v-permission 指令的使用是否正确,确保权限编码存在 |
| 页面刷新后权限丢失 | 确保权限信息在页面刷新后能从缓存或后端重新获取 |
6. 📊 权限流程图
mermaid
sequenceDiagram
participant User as 用户
participant Frontend as 前端
participant Backend as 后端
participant DB as 数据库
User->>Frontend: 登录
Frontend->>Backend: 发送登录请求
Backend->>DB: 查询用户信息和权限
DB-->>Backend: 返回用户信息和权限
Backend-->>Frontend: 返回登录结果和权限列表
Frontend->>Frontend: 存储权限到 Pinia
Frontend->>User: 登录成功,跳转首页
Note over Frontend,Backend: 权限校验流程
User->>Frontend: 访问受保护路由
Frontend->>Frontend: 路由守卫检查权限
Frontend->>User: 允许访问或跳转到无权限页面
User->>Frontend: 点击按钮
Frontend->>Frontend: v-permission 指令检查权限
Frontend->>User: 显示或隐藏按钮