前言

本文将对 Vue-Vben-Admin 角色权限的状态管理进行源码解读,耐心读完,相信您一定会有所收获!

更多系列文章详见专栏   Vben Admin 项目分析&实践 。

本文涉及到角色权限之外的较多内容(路由相关)会一笔带过,具体功能实现将在后面专题中详细讨论。为了更好的理解本文内容,请先阅读官方的文档说明 # 权限

permission.ts 角色权限

文件 src\store\modules\permission.ts 声明导出一个store实例 usePermissionStore 、一个方法 usePermissionStoreWithOut()用于没有使用 setup 组件时使用。

// 角色权限信息存储
export const usePermissionStore = defineStore({
id: 'app-permission',
state: { /*...*/ },
getters: { /*...*/ }
actions:{ /*...*/ }
}); export function usePermissionStoreWithOut() {
return usePermissionStoreWithOut(store);
}

State/Getter

状态对象定义了权限代码列表、是否动态添加路由、菜单最后更新时间、后端角色权限菜单列表以及前端角色权限菜单列表。同时提供了对应getter用于获取状态值。

// 权限状态
interface PermissionState {
permCodeList: string[] | number[]; // 权限代码列表
isDynamicAddedRoute: boolean; // 是否动态添加路由
lastBuildMenuTime: number; // 菜单最后更新时间
backMenuList: Menu[]; // 后端角色权限菜单列表
frontMenuList: Menu[]; // 前端角色权限菜单列表
} // 状态定义及初始化
state: (): PermissionState => ({
permCodeList: [],
isDynamicAddedRoute: false,
lastBuildMenuTime: 0,
backMenuList: [],
frontMenuList: [],
}),
getters: {
getPermCodeList(): string[] | number[] {
return this.permCodeList; // 获取权限代码列表
},
getBackMenuList(): Menu[] {
return this.backMenuList; // 获取后端角色权限菜单列表
},
getFrontMenuList(): Menu[] {
return this.frontMenuList; // 获取前端角色权限菜单列表
},
getLastBuildMenuTime(): number {
return this.lastBuildMenuTime; // 获取菜单最后更新时间
},
getIsDynamicAddedRoute(): boolean {
return this.isDynamicAddedRoute; // 获取是否动态添加路由
},
},

Actions

以下方法用于更新状态属性。

// 更新属性 permCodeList
setPermCodeList(codeList: string[]) {
this.permCodeList = codeList;
},
// 更新属性 backMenuList
setBackMenuList(list: Menu[]) {
this.backMenuList = list;
list?.length > 0 && this.setLastBuildMenuTime(); // 记录菜单最后更新时间
},
// 更新属性 frontMenuList
setFrontMenuList(list: Menu[]) {
this.frontMenuList = list;
},
// 更新属性 lastBuildMenuTime
setLastBuildMenuTime() {
this.lastBuildMenuTime = new Date().getTime(); // 一个代表时间毫秒数的数值
},
// 更新属性 isDynamicAddedRoute
setDynamicAddedRoute(added: boolean) {
this.isDynamicAddedRoute = added;
},
// 重置状态属性
resetState(): void {
this.isDynamicAddedRoute = false;
this.permCodeList = [];
this.backMenuList = [];
this.lastBuildMenuTime = 0;
},

方法 changePermissionCode 模拟从后台获得用户权限码,常用于后端权限模式下获取用户权限码。项目中使用了本地 Mock服务模拟。

async changePermissionCode() {
const codeList = await getPermCode();
this.setPermCodeList(codeList);
}, // src\api\sys\user.ts
enum Api {
GetPermCode = '/getPermCode',
}
export function getPermCode() {
return defHttp.get<string[]>({ url: Api.GetPermCode });
}

使用到的 mock 接口和模拟数据。

// mock\sys\user.ts
{
url: '/basic-api/getPermCode',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
// ...
const checkUser = createFakeUserList().find((item) => item.token === token);
const codeList = fakeCodeList[checkUser.userId];
// ...
return resultSuccess(codeList);
},
}, const fakeCodeList: any = {
'1': ['1000', '3000', '5000'],
'2': ['2000', '4000', '6000'],
};

动态路由&权限过滤

方法buildRoutesAction用于动态路由及用户权限过滤,代码逻辑结构如下:

async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
const { t } = useI18n(); // 国际化
const userStore = useUserStore(); // 用户信息存储
const appStore = useAppStoreWithOut(); // 项目配置信息存储 let routes: AppRouteRecordRaw[] = [];
// 用户角色列表
const roleList = toRaw(userStore.getRoleList) || [];
// 获取权限模式
const { permissionMode = projectSetting.permissionMode } = appStore.getProjectConfig; // 基于角色过滤方法
const routeFilter = (route: AppRouteRecordRaw) => { /*...*/ };
// 基于 ignoreRoute 属性过滤
const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => { /*...*/ }; // 不同权限模式处理逻辑
switch (permissionMode) {
// 前端方式控制(菜单和路由分开配置)
case PermissionModeEnum.ROLE: /*...*/
// 前端方式控制(菜单由路由配置自动生成)
case PermissionModeEnum.ROUTE_MAPPING: /*...*/
// 后台方式控制
case PermissionModeEnum.BACK: /*...*/
} routes.push(ERROR_LOG_ROUTE); // 添加`错误日志列表`页面路由 // 根据设置的首页path,修正routes中的affix标记(固定首页)
const patchHomeAffix = (routes: AppRouteRecordRaw[]) => { /*...*/ };
patchHomeAffix(routes); return routes; // 返回路由列表
},

页面“错误日志列表”路由地址/error-log/list,功能如下:

权限模式

框架提供了完善的前后端权限管理方案,集成了三种权限处理方式:

  1. ROLE 通过用户角色来过滤菜单(前端方式控制),菜单和路由分开配置。
  2. ROUTE_MAPPING通过用户角色来过滤菜单(前端方式控制),菜单由路由配置自动生成。
  3. BACK 通过后台来动态生成路由表(后端方式控制)。
// src\settings\projectSetting.ts
// 项目配置
const setting: ProjectConfig = {
permissionMode: PermissionModeEnum.ROUTE_MAPPING, // 权限模式 默认前端模式
permissionCacheType: CacheTypeEnum.LOCAL, // 权限缓存存放位置 默认存放于localStorage
// ...
} // src\enums\appEnum.ts
// 权限模式枚举
export enum PermissionModeEnum {
ROLE = 'ROLE', // 前端模式(菜单路由分开)
ROUTE_MAPPING = 'ROUTE_MAPPING', // 前端模式(菜单由路由生成)
BACK = 'BACK', // 后端模式
}

前端权限模式

前端权限模式提供了 ROLEROUTE_MAPPING两种处理逻辑,接下来将一一分析。

在前端会固定写死路由的权限,指定路由有哪些权限可以查看。系统定义路由记录时指定可以访问的角色RoleEnum.SUPER

// src\router\routes\modules\demo\permission.ts
{
path: 'auth-pageA',
name: 'FrontAuthPageA',
component: () => import('/@/views/demo/permission/front/AuthPageA.vue'),
meta: {
title: t('routes.demo.permission.frontTestA'),
roles: [RoleEnum.SUPER],
},
},

系统使用meta属性在路由记录上附加自定义数据,它可以在路由地址和导航守卫上都被访问到。本方法中使用到的配置属性如下:

export interface RouteMeta {
// 可以访问的角色,只在权限模式为Role的时候有效
roles?: RoleEnum[];
// 是否固定标签
affix?: boolean;
// 菜单排序,只对第一级有效
orderNo?: number;
// 忽略路由。用于在ROUTE_MAPPING以及BACK权限模式下,生成对应的菜单而忽略路由。
ignoreRoute?: boolean;
// ...
}

ROLE

初始化通用的路由表asyncRoutes,获取用户角色后,通过角色去遍历路由表,获取该角色可以访问的路由表,然后对其格式化处理,将多级路由转换为二级路由,最终返回路由表。

// 前端方式控制(菜单和路由分开配置)
import { asyncRoutes } from '/@/router/routes'; // ... case PermissionModeEnum.ROLE:
// 根据角色过滤路由
routes = filter(asyncRoutes, routeFilter);
routes = routes.filter(routeFilter);
// 将多级路由转换为二级路由
routes = flatMultiLevelRoutes(routes);
break; // src\router\routes\index.ts
export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];

在路由钩子内动态判断,调用方法返回生成的路由表,再通过 router.addRoutes 添加到路由实例,实现权限的过滤。

// src/router/guard/permissionGuard.ts
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
// ....
routeFilter

过滤方法routeFilter通过角色去遍历路由表,获取该角色可以访问的路由表。

const userStore = useUserStore(); // 用户信息存储
const roleList = toRaw(userStore.getRoleList) || []; // 用户角色列表 const routeFilter = (route: AppRouteRecordRaw) => {
const { meta } = route;
const { roles } = meta || {};
if (!roles) return true;
return roleList.some((role) => roles.includes(role));
};
flatMultiLevelRoutes

方法flatMultiLevelRoutes将多级路由转换为二级路由,下图是未处理前路由表信息:

下图是格式化后的二级路由表信息:

ROUTE_MAPPING

ROUTE_MAPPINGROLE逻辑一样,不同之处会根据路由自动生成菜单。

// 前端方式控制(菜单由路由配置自动生成)
case PermissionModeEnum.ROUTE_MAPPING:
// 根据角色过滤路由
routes = filter(asyncRoutes, routeFilter);
routes = routes.filter(routeFilter);
// 通过转换路由生成菜单
const menuList = transformRouteToMenu(routes, true);
// 移除属性 meta.ignoreRoute 路由
routes = filter(routes, routeRemoveIgnoreFilter);
routes = routes.filter(routeRemoveIgnoreFilter);
menuList.sort((a, b) => {
return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0);
}); // 通过转换路由生成菜单
this.setFrontMenuList(menuList);
// 将多级路由转换为二级路由
routes = flatMultiLevelRoutes(routes);
break;

调用方法 transformRouteToMenu 将路由转换成菜单,调用过滤方法routeRemoveIgnoreFilter忽略设置ignoreRoute属性的路由菜单。

const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => {
const { meta } = route;
const { ignoreRoute } = meta || {};
return !ignoreRoute;
};

系统示例,路由下不同的路径参数生成一个菜单。

// src\router\routes\modules\demo\feat.ts
{
path: 'testTab/:id',
name: 'TestTab',
component: () => import('/@/views/demo/feat/tab-params/index.vue'),
meta: {
hidePathForChildren: true,
},
children: [
{
path: 'testTab/id1',
name: 'TestTab1',
component: () => import('/@/views/demo/feat/tab-params/index.vue'),
meta: {
ignoreRoute: true,
},
},
{
path: 'testTab/id2',
name: 'TestTab2',
component: () => import('/@/views/demo/feat/tab-params/index.vue'),
meta: {
ignoreRoute: true,
},
},
],
},

BACK 后端权限模式

ROUTE_MAPPING逻辑处理相似,只不过路由表数据来源是调用接口从后台获取。

// 后台方式控制
case PermissionModeEnum.BACK:
let routeList: AppRouteRecordRaw[] = []; // 获取后台返回的菜单配置
this.changePermissionCode(); // 模拟从后台获取权限码
routeList = (await getMenuList()) as AppRouteRecordRaw[]; // 模拟从后台获取菜单信息
// 基于路由动态地引入相关组件
routeList = transformObjToRoute(routeList);
// 通过路由列表转换成菜单
const backMenuList = transformRouteToMenu(routeList);
// 设置菜单列表
this.setBackMenuList(backMenuList); // 移除属性 meta.ignoreRoute 路由
routeList = filter(routeList, routeRemoveIgnoreFilter);
routeList = routeList.filter(routeRemoveIgnoreFilter); // 将多级路由转换为二级路由
routeList = flatMultiLevelRoutes(routeList);
routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];
break;

参考&关联阅读

"routelocationnormalized",vue-router

"Meta 配置说明",vvbin.cn

"Date/getTime",MDN

"toraw",vuejs

关注专栏

如果本文对您有所帮助请关注、 点赞、 收藏!您的认可就是对我的最大支持!

此文章已收录到专栏中 ,可以直接关注。

Vben Admin 源码学习:状态管理-角色权限的更多相关文章

  1. Vben Admin 源码学习:状态管理-错误日志

    0x00 前言 本文将对 Vue-Vben-Admin 的状态管理实现源码进行分析解读,耐心读完,相信您一定会有所收获! 0x01 errorLog.ts 错误日志 文件 src\store\modu ...

  2. Vben Admin 源码学习:项目初始化

    0x00 前言 Vue-Vben-Admin 是一个免费开源的中后台模版.使用了最新的vue3,vite2,TypeScript等主流技术开发,开箱即用的中后台前端解决方案考. 本系列本着学习参考的目 ...

  3. Linux 2.6 源码学习-内存管理-buddy算法

    核心数据结构 linux 2.6 的内存管理支持NUMA(Non Uniform Memory Access Achitecture),即非一致内存访问体系,在该体系中存在多个CPU,并且拥有分离的存 ...

  4. 框架源码系列十一:事务管理(Spring事务管理的特点、事务概念学习、Spring事务使用学习、Spring事务管理API学习、Spring事务源码学习)

    一.Spring事务管理的特点 Spring框架为事务管理提供一套统一的抽象,带来的好处有:1. 跨不同事务API的统一的编程模型,无论你使用的是jdbc.jta.jpa.hibernate.2. 支 ...

  5. Spring5.0源码学习系列之事务管理概述

    Spring5.0源码学习系列之事务管理概述(十一),在学习事务管理的源码之前,需要对事务的基本理论比较熟悉,所以本章节会对事务管理的基本理论进行描述 1.什么是事务? 事务就是一组原子性的SQL操作 ...

  6. Qt Creator 源码学习笔记03,大型项目如何管理工程

    阅读本文大概需要 6 分钟 一个项目随着功能开发越来越多,项目必然越来越大,工程管理成本也越来越高,后期维护成本更高.如何更好的组织管理工程,是非常重要的 今天我们来学习下 Qt Creator 是如 ...

  7. mongo源码学习(三)请求接收传输层

    在上一篇博客中(mongo源码学习(二)db.cpp之mongoDbMain方法分析),我们把db.cpp中的mongoDbMain的执行过程分析了一下,最后会调用initAndListen(serv ...

  8. MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)

    前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...

  9. MVC系列——MVC源码学习:打造自己的MVC框架(一:核心原理)

    前言:最近一段时间在学习MVC源码,说实话,研读源码真是一个痛苦的过程,好多晦涩的语法搞得人晕晕乎乎.这两天算是理解了一小部分,这里先记录下来,也给需要的园友一个参考,奈何博主技术有限,如有理解不妥之 ...

随机推荐

  1. VirtualBox虚拟机安装Ubuntu系统后,增加内存空间和处理器核心数

    对于Linux爱好者而言,初次使用虚拟机时,一般都会使用默认的设置,例如硬盘空间.内存空间等等. 而往往在熟悉之后,安装了某些必要的软件,或者熟悉了实际的开发场景后,却发现原本给虚拟机分配的物理资源是 ...

  2. Java中Double类型数据比较大小

    方法一:转成字符串之后比较 如果要比较的两个double数据的字符串精度相等,可以将数据转换成string然后借助string的equals方法来间接实现比较两个double数据是否相等.注意这种方法 ...

  3. .NET ORM框架HiSql实战-第一章-集成HiSql

    一.引言 做.Net这么多年,出现了很多很多ORM框架,比如Dapper,Sqlsugar,Freesql等等.在之前的项目中,用到的ORM框架也大多数是这几个老牌的框架. 不过最近园子关于.NET ...

  4. HBuilderX配置外部服务器(tomcat)查看编辑jsp界面

    HBuilderX配置外部服务器(tomcat)查看编辑jsp界面 一.第一种方法,通过启动本地tomcat,查看jsp 在tomcat的webapps目录下创建文件夹HBuilderX 打开HBui ...

  5. P3480 [POI2009]KAM-Pebbles 题解

    题目链接 首先,这道题看上去就是个博弈论,很显然的 \(Nim\) 游戏. 因为每一个的取法都和它的上一位有关. 有一种非常显然的转换方式 :我们把这若干堆石子从前向后做一个差分 . 我们记 \(a_ ...

  6. 基于MATLAB静态目标分割的药板胶囊检测

    一.目标 1 将药板从黑色背景中分离(药板部分显示为白色,背景显示为黑色): 2 根据分割结果将药板旋转至水平: 3 提取药板中的药丸的位置信息: 二.方法描述 处理图像如下: (1)首先将图像转为灰 ...

  7. 索尼笔记本Linux系统唤醒后,键盘无法使用

    1.编辑grub文件 sudo gedit /etc/default/grub 2.修改成以下参数 GRUB_CMDLINE_LINUX_DEFAULT="quiet splash i804 ...

  8. 7 什么是dubbo

    什么是dubbo 快速入门dubbo 了解什么是dubbo之前,我们得先了解什么是分布式系统? <分布式系统原理与范型>定义: 分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像 ...

  9. SpringBoot到底是什么?

    摘要:Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程. 本文分享自华为云社区<SpringBoot到底是什么?如何理解p ...

  10. Keep In Line_via牛客网

    题目 链接:https://ac.nowcoder.com/acm/contest/28537/H 来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 262144K,其他语 ...