【Vue】权限控制

权限管理

分类:

  1. 页面权限
  2. 功能(按钮)权限
  3. 接口权限

vue3-element-admin 的实现方案

一般我们在业务中将 路由可以分为两种,constantRoutesasyncRoutes

  • constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404(或者不存在的路由)、首页、数据大屏等通用页面。

  • asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。

后台管理系统中的路由都具有不同的访问权限,侧边菜单栏也是同理,需要根据权限,异步生成。

整体步骤都十分类似:

我们在登录后获取 token ,将其存入 localStorage 中,用来“象征用户身份”。

登录表单提交业务实现:

/** 登录表单提交 */
function handleLoginSubmit() {loginFormRef.value?.validate((valid: boolean) => {if (valid) {loading.value = true;userStore.login(loginData.value).then(() => {const { path, queryParams } = parseRedirect();router.push({ path: path, query: queryParams });}).catch(() => {getCaptcha();}).finally(() => {loading.value = false;});}});
}

调用登录接口,存储 token 到localStorage 中。

/*** 登录* @param {LoginData}* @returns*/
function login(loginData: LoginData) {return new Promise<void>((resolve, reject) => {AuthAPI.login(loginData).then((data) => {const { tokenType, accessToken } = data;localStorage.setItem(TOKEN_KEY, tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxxresolve();}).catch((error) => {reject(error);});});
}

登录接口。

class AuthAPI {/** 登录 接口*/static login(data: LoginData) {const formData = new FormData();formData.append("username", data.username);formData.append("password", data.password);formData.append("captchaKey", data.captchaKey);formData.append("captchaCode", data.captchaCode);return request<any, LoginResult>({url: "/api/v1/auth/login",method: "post",data: formData,headers: {"Content-Type": "multipart/form-data",},});}// ...
}

获取验证码。

/** 获取验证码 */
function getCaptcha() {AuthAPI.getCaptcha().then((data) => {loginData.value.captchaKey = data.captchaKey;captchaBase64.value = data.captchaBase64;});
}

通过上述过程,我们已经成功获取 token 并存储在了 localStorage 中。

之后我们就可以根据 token “用户身份” 来进行权限控制了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

token 可以获取用户角色,而不同角色对应不同权限的路由,然后通过 router.addRoutes 动态挂载路由。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用获取路由接口,获取路由表,进行动态路由处理,将其与常量路由进行拼接,得到总路由。

/*** 生成动态路由*/
function generateRoutes() {return new Promise<RouteRecordRaw[]>((resolve, reject) => {MenuAPI.getRoutes().then((data) => {const dynamicRoutes = transformRoutes(data);routes.value = constantRoutes.concat(dynamicRoutes);resolve(dynamicRoutes);}).catch((error) => {reject(error);});});
}

这里使用 mock 数据:

export default defineMock([{url: "menus/routes",method: ["GET"],body: {code: "00000",data: [{path: "/doc",component: "Layout",redirect: "https://juejin.cn/post/7228990409909108793",name: "/doc",meta: {title: "平台文档",icon: "document",hidden: false,alwaysShow: false,params: null,},children: [{path: "internal-doc",component: "demo/internal-doc",name: "InternalDoc",meta: {title: "平台文档(内嵌)",icon: "document",hidden: false,alwaysShow: false,params: null,},},{path: "https://juejin.cn/post/7228990409909108793",name: "Https://juejin.cn/post/7228990409909108793",meta: {title: "平台文档(外链)",icon: "link",hidden: false,alwaysShow: false,params: null,},},],},{path: "/multi-level",component: "Layout",name: "/multiLevel",meta: {title: "多级菜单",icon: "cascader",hidden: false,alwaysShow: true,params: null,},children: [{path: "multi-level1",component: "demo/multi-level/level1",name: "MultiLevel1",meta: {title: "菜单一级",icon: "",hidden: false,alwaysShow: true,params: null,},children: [{path: "multi-level2",component: "demo/multi-level/children/level2",name: "MultiLevel2",meta: {title: "菜单二级",icon: "",hidden: false,alwaysShow: false,params: null,},children: [{path: "multi-level3-1",component: "demo/multi-level/children/children/level3-1",name: "MultiLevel31",meta: {title: "菜单三级-1",icon: "",hidden: false,keepAlive: true,alwaysShow: false,params: null,},},{path: "multi-level3-2",component: "demo/multi-level/children/children/level3-2",name: "MultiLevel32",meta: {title: "菜单三级-2",icon: "",hidden: false,keepAlive: true,alwaysShow: false,params: null,},},],},],},],},],msg: "一切ok",},},
]);

转换路由数据为组件(根据实际业务进行弹性操作)。

/*** 转换路由数据为组件*/
const transformRoutes = (routes: RouteVO[]) => {const asyncRoutes: RouteRecordRaw[] = [];routes.forEach((route) => {const tmpRoute = { ...route } as RouteRecordRaw;// 顶级目录,替换为 Layout 组件if (tmpRoute.component?.toString() == "Layout") {tmpRoute.component = Layout;} else {// 其他菜单,根据组件路径动态加载组件const component = modules[`../../views/${tmpRoute.component}.vue`];if (component) {tmpRoute.component = component;} else {tmpRoute.component = modules[`../../views/error-page/404.vue`];}}if (tmpRoute.children) {tmpRoute.children = transformRoutes(route.children);}asyncRoutes.push(tmpRoute);});return asyncRoutes;
};

vue-element-admin 的实现方案

当然他们只是实现的写法不同,大致的思路还是相同的。

为了便于理解主要思路和提取关键代码,下面使用尚硅谷硅谷甄选项目的实现方案代码讲解(和vue-element-admin的大差不差)。

先看下用户信息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在用户 store 中过滤用户的异步路由(用路由的名字进行过滤区分-所以要保证名字的唯一):

//用于过滤当前用户需要展示的异步路由
// asyncRoute 所有异步路由 routes 用户拥有权限的路由
function filterAsyncRoute(asnycRoute: any, routes: any) {return asnycRoute.filter((item: any) => {if (routes.includes(item.name)) {if (item.children && item.children.length > 0) {// 新的 item.children 也需要进行同样的过滤操作 item.children = filterAsyncRoute(item.children, routes);}return true;}})
}

获取用户个人信息后再 store 中操作路由:

//计算当前用户需要展示的异步路由
let userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute), result.data.routes);
//菜单需要的数路由数据
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute];
//目前路由器管理的只有常量路由,再异步获取路由路由信息后,异步路由、任意路由动态追加到路由管理中
[...userAsyncRoute, anyRoute].forEach((route: any) => {router.addRoute(route);
});

路由表:

//对外暴露配置路由(常量路由):全部用户都可以访问到的路由
export const constantRoute = [{//登录path: '/login',component: () => import('@/views/login/index.vue'),name: 'login',meta: {title: '登录',//菜单标题hidden: true,//代表路由标题在菜单中是否隐藏  true:隐藏 false:不隐藏icon: "Promotion",//菜单文字左侧的图标,支持element-plus全部图标}},{//登录成功以后展示数据的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {title: '',hidden: false,icon: ''},redirect: '/home',children: [{path: '/home',component: () => import('@/views/home/index.vue'),meta: {title: '首页',hidden: false,icon: 'HomeFilled'}}]},{//404path: '/404',component: () => import('@/views/404/index.vue'),name: '404',meta: {title: '404',hidden: true,icon: 'DocumentDelete'}},{path: '/screen',component: () => import('@/views/screen/index.vue'),name: 'Screen',meta: {hidden: false,title: '数据大屏',icon: 'Platform'}}]//异步路由
export const asnycRoute = [{path: '/acl',component: () => import('@/layout/index.vue'),name: 'Acl',meta: {title: '权限管理',icon: 'Lock'},redirect: '/acl/user',children: [{path: '/acl/user',component: () => import('@/views/acl/user/index.vue'),name: 'User',meta: {title: '用户管理',icon: 'User'}},{path: '/acl/role',component: () => import('@/views/acl/role/index.vue'),name: 'Role',meta: {title: '角色管理',icon: 'UserFilled'}},{path: '/acl/permission',component: () => import('@/views/acl/permission/index.vue'),name: 'Permission',meta: {title: '菜单管理',icon: 'Monitor'}}]},{path: '/product',component: () => import('@/layout/index.vue'),name: 'Product',meta: {title: '商品管理',icon: 'Goods',},redirect: '/product/trademark',children: [{path: '/product/trademark',component: () => import('@/views/product/trademark/index.vue'),name: "Trademark",meta: {title: '品牌管理',icon: 'ShoppingCartFull',}},{path: '/product/attr',component: () => import('@/views/product/attr/index.vue'),name: "Attr",meta: {title: '属性管理',icon: 'ChromeFilled',}},{path: '/product/spu',component: () => import('@/views/product/spu/index.vue'),name: "Spu",meta: {title: 'SPU管理',icon: 'Calendar',}},{path: '/product/sku',component: () => import('@/views/product/sku/index.vue'),name: "Sku",meta: {title: 'SKU管理',icon: 'Orange',}},]}
]//任意路由
export const anyRoute = {//任意路由path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any',meta: {title: '任意路由',hidden: true,icon: 'DataLine'}
}

路由器对象(初始化的时候只注册了常量路由):

//创建路由器
let router = createRouter({//路由模式hashhistory: createWebHashHistory(),routes: constantRoute,//滚动行为scrollBehavior() {return {left: 0,top: 0}}
});

路由鉴权守卫(这里在某些业务情况下可以增加白名单,对权限进行再一次划分):

//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router';
import setting from './setting';
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
nprogress.configure({ showSpinner: false });
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user';
import pinia from './store';
let userStore = useUserStore(pinia);
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {document.title = `${setting.title} - ${to.meta.title}`//to:你将要访问那个路由//from:你从来个路由而来//next:路由的放行函数nprogress.start();//获取token,去判断用户登录、还是未登录let token = userStore.token;//获取用户名字let username = userStore.username;//用户登录判断if (token) {//登录成功,访问login,不能访问,指向首页if (to.path == '/login') {next({ path: '/' })} else {//登录成功访问其余六个路由(登录排除)//有用户信息if (username) {//放行next();} else {//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行try {//获取用户信息await userStore.userInfo();//放行//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果next({...to,replace:true})} catch (error) {//token过期:获取不到用户信息了//用户手动修改本地存储token//退出登录->用户相关的数据清空await userStore.userLogout();next({ path: '/login', query: { redirect: to.path } })}}}} else {//用户未登录判断if (to.path == '/login') {next();} else {next({ path: '/login', query: { redirect: to.path } });}}
})
//全局后置守卫
router.afterEach((to: any, from: any) => {nprogress.done();
});//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问

这里用到了 next({…to,replace:true}) ,它的原理是:

router.addRoutes是同步方法,整体流程:

  1. 路由跳转,根据目标地址从router中提取route信息,由于此时还没addRouters,所以解析出来的route是个空的,不包含组件。
  2. 执行beforeEach钩子函数,然后内部会动态添加路由,但此时route已经生成了,不是说router.addRoutes后,这个route会自动更新,如果直接next(),最终渲染的就是空的。
  3. 调用next({ …to, replace: true }),会abort刚刚的跳转,然后重新走一遍上述逻辑,这时从router中提取的route信息就包含组件了,之后就和正常逻辑一样了。
    主要原因就是生成route是在执行beforeEach钩子之前。

上述解释摘自手摸手,带你用vue撸后台 系列二(登录权限篇) - 掘金 (juejin.cn)评论区

页面权限总结

页面权限:

  1. 用户登录后,服务端返回一个权限树(用树形结构呈现权限数据),然后我们去解析这个树形结构,得到我们需要的路由表(动态路由对象),本质上就是一个由路由对象为元素的数组。
  2. 然后通过 vue 中的动态路由,也就是 addRoutes,动态的添加路由。
  3. 最后根据路由去渲染多级菜单栏。

按钮权限

可以自定义一个全局指定,用于按钮权限的判断。

import pinia from '@/store';
import useUserStore from '@/store/modules/user';
let userStore =useUserStore(pinia)
export const isHasButton = (app: any) => {//获取对应的用户仓库//全局自定义指令:实现按钮的权限app.directive('has', {//代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次mounted(el:any,options:any) {//自定义指令右侧的数值:如果在用户信息buttons数组当中没有//从DOM树上干掉if(!userStore.buttons.includes(options.value)){el.parentNode.removeChild(el);}},})
}

el 为该元素,options 为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 app.ts 中引入。

import {isHasButton} from '@/directive/has.ts';
isHasButton(app);

使用:

v-has="`btn.Trademark.add`"

按钮(功能)权限总结

服务端返回的权限树中包含了指定页面下指定按钮的数据,可以通过 v-if 或者 disable 来控制按钮权限。

接口权限

配合功能权限,一般由服务端进行处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/877331.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【echarts】 柱状图,最后带“竖线”

具体&#xff1a; https://echarts.zhangmuchen.top/#/detail?cid28ea6-0601-e9f5-9cc29-c022b758 let data [{value: 0,name: 数据格式一},{value: 55,name: 数据格式二},{value: 66,name: 数据格式三},{value: 75,name: 数据格式四},{value: 20,name: 数据格式五}];getAr…

盘点.软件测试模型

软件开发模型   软件开发模型(Software Development Model)是指软件开发全部过程、活动和任务的结构框架。软件开发包括需求、设计、编码和测试等阶段&#xff0c;有时也包括维护阶段。 软件开发模型能清晰、直观地表达软件开发全过程&#xff0c;明确规定了要完成的主要活动…

C++类与对象-六大成员函数

默认成员函数就是用户没有显式实现&#xff0c;编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个空类编译器会默认⽣成8个默认成员函数。本文只介绍其中6个&#xff0c;C11增加两个函数见后续博客。 目录 一、构造函数 1.1 概念 1.2 特性 1.3 使用举例 1.4 初始化列表 1…

如何通过前端表格控件实现自动化报表?

背景 最近伙伴客户的项目经理遇见一个问题&#xff0c;他们在给甲方做自动化报表工具&#xff0c;项目已经基本做好了&#xff0c;但拿给最终甲方&#xff0c;业务人员不太买账&#xff0c;项目经理为此也是天天抓狂&#xff0c;没有想到合适的应对方案。 现阶段主要面临的问…

docker笔记7-dockerfile

docker笔记7-dockerfile 一、dockerfile介绍二、dockerfile指令三、构建自己的镜像 一、dockerfile介绍 Dockerfile是用来构建Docker镜像的构建文件&#xff0c;是由一系列命令和参数构成的脚本。 以下是常用的 Dockerfile 关键字的完整列表和说明&#xff1a; 二、docker…

【计算机毕业设计】838装修公司CRM系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

为什么要做边界值测试?

边界值测试的理解 边界值测试&#xff08;Boundary Value Testing&#xff09;是一种常用的软件测试方法&#xff0c;它侧重于测试输入值的边缘或临界条件。这些边缘条件通常包括最小值、最大值以及接近这些最小值和最大值的值。边界值测试的基本思想是&#xff0c;许多软件错…

React 学习——Context机制层级组件通信

核心思路&#xff1a;&#xff08;适用于所有层级&#xff0c;不仅仅是爷孙 父子&#xff09; createContext方法创建一个上下文对象在顶层组件 通过Provider组件提供数据在底层组件&#xff0c;通过useContext钩子函数使用数据 import { createContext, useContext } from …

R语言 爬取数据+简单清洗

小小练习。见代码注释 # 加载必要的包 library(rvest) library(dplyr) library(tidyr)# 指定网页URL url <- "https://research.un.org/en/unmembers/scmembers"# 读取网页内容 webpage <- read_html(url)# 提取所有表格节点 table_nodes <- html_nodes(web…

基于人工智能及大数据的综合智能交通管理平台(可编辑30页PPT)

引言&#xff1a;随着城市化进程的加速和汽车保有量的快速增长&#xff0c;交通拥堵、交通事故频发以及交通资源分配不均等问题日益突出&#xff0c;成为制约城市发展的重要因素。为了应对这些挑战&#xff0c;基于人工智能&#xff08;AI&#xff09;及大数据技术的综合智能交…

【React】详解自定义 Hook

文章目录 一、自定义 Hook 的基本用法1. 什么是自定义 Hook&#xff1f;2. 创建自定义 Hook3. 使用自定义 Hook 二、自定义 Hook 的进阶应用1. 处理副作用2. 组合多个 Hook3. 参数化 Hook4. 条件逻辑 三、自定义 Hook 的实际应用案例1. 实现用户身份验证2. 实现媒体查询 四、最…

民大食堂用餐小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;商家管理&#xff0c;档口号管理&#xff0c;商家餐品管理&#xff0c;餐品种类管理&#xff0c;购物车管理&#xff0c;订单信息管理 微信端账号功能包括&#xff1a;系统首页&a…

angular入门基础教程(七)系统路由

路由的实现 当我们系统越来复杂&#xff0c;功能越来越多&#xff0c;路由也就是必须的了。在 ng 中如何实现路由呢&#xff1f; 启用路由 在 app 目录下&#xff0c;新建一个 router 目录&#xff0c;把 app.routers.ts 文件拷贝过来&#xff0c;并修改一下。 import { Ro…

C语言程序设计16

程序设计16 问题16_1代码16_1结果16_1 问题16_2代码16_2结果16_2 问题16_3代码16_3结果16_3 问题16_1 函数 f u n fun fun 的功能是&#xff1a;逆置数组元素中的值。 例如&#xff0c;若形参 a a a 所指数组中的数据最初排列为 &#xff1a; 1 , 2 , 3 , 4 , 5 , 6 …

高职院校大数据人才培养成果导向系统构建、实施要点与评量方法

一、引言 在当今信息化快速发展的背景下&#xff0c;大数据已成为推动社会进步和产业升级的重要力量。为满足社会对大数据人才的需求&#xff0c;高职院校纷纷开设大数据相关专业&#xff0c;并致力于探索科学有效的人才培养模式。本文立足于我国信息化与智能化发展趋势&#…

【初阶数据结构】10.排序(1)

文章目录 1.排序概念及运用1.1 概念1.2 运用1.3 常见排序算法 2. 实现常见排序算法2.1 插入排序2.1.1 直接插入排序2.1.2 希尔排序2.1.2.1 希尔排序的时间复杂度计算 2.2 选择排序2.2.1 直接选择排序2.2.2 堆排序 1.排序概念及运用 1.1 概念 排序&#xff1a;所谓排序&#x…

如何用PostMan按照规律进行循环访问接口

①设置动态变量 步骤一: 设置环境变量 1. 创建环境变量集合 在 Postman 左上角选择 "环境"&#xff0c;然后点击 "添加" 来创建一个新的环境变量集合。给它起一个名称&#xff0c;比如 "uploadDemo". 2. 添加初始变量 在新创建的环境变量集…

基于python的百度迁徙迁入、迁出数据分析(三)

百度迁徙定义 百度迁徙释义&#xff1a; 百度迁徙以用户常住地所在地市或停留超过一天的非常住地定义为出发城市&#xff0c;以用户离开出发城市&#xff0c;并在非出发城市停留超过4 h以上定义为到达城市。采用4h阈值&#xff0c;排除了城际出行中的途经地。 定义参考来源…

filament 初使用记录

安装初始化 一、环境准备 官网要的 我安装的 二、下载安装 安装laravel composer create-project --prefer-dist laravel/laravel 项目名称 10.*导入 filament composer require filament/filament注册 filament 管理面板 php artisan filament:install --panels初始化…

freertos-HAL库-STM32Cubemax生成

打开cubemax选好型号配置RCC&#xff08;外部高速时钟&#xff09;这里查看原理图&#xff0c;我们把按键设为输入&#xff0c;led设为输出创建两个新任务&#xff08;default是系统创建的&#xff09;配置时钟&#xff0c;这里HSE是外部高速时钟&#xff0c;HSI是内部的&#…