vue权限管理解决方案

news2025/2/27 13:15:39

在这里插入图片描述

一. 什么是权限管理

权限控制是确保用户只能访问其被授权的资源和执行其被授权的操作的重要方面。而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发

  • 页面加载触发
  • 页面上的按钮点击触发

总体而言,权限控制可以从前端路由和视图两个方面入手,以确保对触发权限的源头进行有效的控制。最终目标是确保用户在整个应用程序中的访问和操作都受到适当的权限限制,从而保护敏感数据和功能:

  • 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
  • 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
  • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

二、如何实现权限管理

前端权限控制通常涉及以下四个方面:

  1. 数据权限控制:

    • 控制用户能够查看或操作的数据范围。例如,在表格中只显示用户有权限查看的数据,或者在数据提交时验证用户是否有权限进行修改。
  2. 按钮/操作权限控制:

    • 控制用户在页面上能否执行某些操作,例如按钮的点击、表单的提交等。这通常需要在页面渲染时根据用户权限动态显示或隐藏相应的按钮或操作元素。
  3. 路由权限控制:

    • 控制用户能否访问特定页面或路由。这涉及到在路由跳转之前进行权限判断,确保用户只能访问其有权限的页面。
  4. 菜单权限控制:

    • 控制用户在系统菜单中能够看到和访问哪些菜单项。这确保用户只能看到其有权限访问的功能模块。

这四个方面的权限控制通常需要配合后端进行,因为前端的权限控制只是一种辅助手段,真正的权限验证和控制应该在后端进行。前端的权限控制主要是为了提升用户体验,使用户在界面上只看到和能够操作其有权限的内容。在实现这些权限控制时,通常会使用一些全局路由守卫、指令、状态管理等技术手段。

接口权限

接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录。 登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token。

// Axios请求拦截器
axios.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

路由权限控制

  1. 方案一

    初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

    import { RouteRecordRaw } from 'vue-router';
    
    const routerMap: RouteRecordRaw[] = [
      {
        path: '/permission',
        component: () => import('@/layouts/Layout.vue'),
        redirect: '/permission/index',
        children: [
          {
            path: 'index',
            component: () => import('@/views/permission/index.vue'),
            name: 'Permission',
            meta: {
              title: 'permission',
              icon: 'lock',
              roles: ['admin', 'editor'], // you can set roles in root nav
              alwaysShow: true, // will always show the root menu
            },
          },
          {
            path: 'page',
            component: () => import('@/views/permission/page.vue'),
            name: 'PagePermission',
            meta: {
              title: 'pagePermission',
              roles: ['admin'], // or you can only set roles in sub nav
            },
          },
          {
            path: 'directive',
            component: () => import('@/views/permission/directive.vue'),
            name: 'DirectivePermission',
            meta: {
              title: 'directivePermission',
              // if do not set roles, means: this page does not require permission
            },
          },
        ],
      },
    ];
    export default routerMap;
    

    这种方式存在以下四种缺点:

    • 性能开销: 如果应用中有大量的路由和权限信息,一次性加载所有路由可能导致初始加载时的性能开销较大,影响应用的启动速度。
    • 安全性: 在前端进行的权限校验可以被绕过,因为前端代码是可见和可修改的。真正的安全性应该在后端进行验证,前端的权限控制主要是为了提升用户体验而非安全性。
    • 维护成本: 随着应用的扩展,维护所有路由和权限信息可能变得复杂。新增、删除或修改路由需要在前端进行手动更新,可能导致潜在的人为错误。
    • 资源浪费: 如果某些页面很少被访问,一次性加载所有路由可能会浪费一些资源,因为用户可能永远不会访问这些页面。
    • 耦合度高: 将路由和权限信息紧密耦合在一起可能会导致代码的可维护性降低,特别是在大型应用中。
  2. 方案二

    初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制

    登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

    import router from './router';
    import store from './store';
    import { Message } from 'element-ui';
    import NProgress from 'nprogress'; // progress bar
    import 'nprogress/nprogress.css'; // progress bar style
    import { getToken } from '@/utils/auth'; // getToken from cookie
    import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
    
    NProgress.configure({ showSpinner: false }); // NProgress Configuration
    
    // permission judge function
    function hasPermission(roles: string[], permissionRoles: string[] | undefined): boolean {
      if (roles.indexOf('admin') >= 0) return true; // admin permission passed directly
      if (!permissionRoles) return true;
      return roles.some(role => permissionRoles.indexOf(role) >= 0);
    }
    
    const whiteList = ['/login', '/authredirect']; // no redirect whitelist
    
    router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
      NProgress.start(); // start progress bar
    
      if (getToken()) { // determine if there has token
        /* has token*/
        if (to.path === '/login') {
          next({ path: '/' });
          NProgress.done(); // if the current page is dashboard will not trigger afterEach hook, so manually handle it
        } else {
          if (store.getters.roles.length === 0) { // determine if the current user has pulled the user_info information
            store.dispatch('GetUserInfo').then(res => { // pull user_info
              const roles = res.data.roles; // note: roles must be an array, such as: ['editor','develop']
              store.dispatch('GenerateRoutes', { roles }).then(() => { // generate accessible route table based on roles
                router.addRoute(store.getters.addRouters[0]); // dynamically add accessible route table
                next({ ...to, replace: true }); // hack method to ensure that addRoutes has completed, set the replace: true so the navigation will not leave a history record
              });
            }).catch((err) => {
              store.dispatch('FedLogOut').then(() => {
                Message.error(err || 'Verification failed, please login again');
                next({ path: '/' });
              });
            });
          } else {
            // If there is no need to dynamically change permissions, you can directly call next() to delete the following permission judgment ↓
            if (hasPermission(store.getters.roles, to.meta.roles)) {
              next();
            } else {
              next({ path: '/401', replace: true, query: { noGoBack: true } });
            }
            // You can delete above ↑
          }
        }
      } else {
        /* has no token*/
        if (whiteList.indexOf(to.path) !== -1) { // in the login whitelist, enter directly
          next();
        } else {
          next('/login'); // otherwise, redirect all to the login page
          NProgress.done(); // if the current page is login will not trigger the afterEach hook, so manually handle it
        }
      }
    });
    
    router.afterEach(() => {
      NProgress.done(); // finish progress bar
    });
    

    按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

    这种方式也存在了以下的缺点:

    • 全局路由守卫里,每次路由跳转都要做判断
    • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
    • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限通常指在一个应用程序或系统中,对用户或用户组在系统菜单中的访问和操作进行控制的功能。具体来说,菜单权限包括了用户能够看到和操作的菜单项、导航链接或按钮等。

  1. 方案一

    菜单与路由分离,菜单由后端返回

    前端定义路由信息

    {
        name: "login",
        path: "/login",
        component: () => import("@/pages/Login.vue")
    }
    

    name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

    全局路由守卫里做判断

    import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router';
    import { ElMessage } from 'element-plus';
    import { getToken } from '@/utils/auth'; // getToken from cookie
    import store from '@/store';
    import Util from '@/utils/util';
    
    const whiteList = ['/login', '/authredirect']; // no redirect whitelist
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes: [],
    });
    
    function hasPermission(route: RouteRecordRaw, accessMenu: any[]): boolean {
      if (whiteList.indexOf(route.path) !== -1) {
        return true;
      }
    
      const menu = Util.getMenuByName(route.name as string, accessMenu);
    
      if (menu.name) {
        return true;
      }
    
      return false;
    }
    
    router.beforeEach(async (to, from, next) => {
      if (getToken()) {
        const userInfo = store.state.user.userInfo;
        if (!userInfo.name) {
          try {
            await store.dispatch('GetUserInfo');
            await store.dispatch('updateAccessMenu');
            if (to.path === '/login') {
              next({ name: 'home_index' });
            } else {
              next({ ...to, replace: true }); // 菜单权限更新完成,重新进入当前路由
            }
          } catch (e) {
            if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
              next();
            } else {
              next('/login');
            }
          }
        } else {
          if (to.path === '/login') {
            next({ name: 'home_index' });
          } else {
            if (hasPermission(to, store.getters.accessMenu)) {
              Util.toDefaultPage(store.getters.accessMenu, to, router.options.routes as RouteRecordRaw[], next);
            } else {
              next({ path: '/403', replace: true });
            }
          }
        }
      } else {
        if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
          next();
        } else {
          next('/login');
        }
      }
    
      const menu = Util.getMenuByName(to.name as string, store.getters.accessMenu);
      Util.title(menu.title);
    });
    
    router.afterEach((to) => {
      window.scrollTo(0, 0);
    });
    
    export default router;
    

    每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的

    如果根据路由name找不到对应的菜单,就表示用户有没权限访问

    如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载

    这种方式的缺点:

    • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
    • 全局路由守卫里,每次路由跳转都要做判断
  2. 方案二

    菜单和路由都由后端返回

    前端统一定义路由组件

    const Home = () => import("../pages/Home.vue");
    const UserInfo = () => import("../pages/UserInfo.vue");
    export default {
        home: Home,
        userInfo: UserInfo
    };
    

    后端路由组件返回以下格式

    [
        {
            name: "home",
            path: "/",
            component: "home"
        },
        {
            name: "home",
            path: "/userinfo",
            component: "userInfo"
        }
    ]
    

    在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件

    如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

    这种方法也会存在缺点:

    • 全局路由守卫里,每次路由跳转都要做判断
    • 前后端的配合要求更高

按钮权限

  1. 方案一 按钮权限也可以用v-if判断

但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断

  1. 方案二

通过自定义指令进行按钮权限的判断

首先配置路由

{
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: {
        btnPermissions: ['admin', 'supper', 'normal']
    },
    //页面需要的权限
    children: [{
        path: 'supper',
        component: _import('system/supper'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin', 'supper']
        } //页面需要的权限
    },
    {
        path: 'normal',
        component: _import('system/normal'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin']
        } //页面需要的权限
    }]
}

自定义权限鉴定指令

import { DirectiveBinding } from 'vue';

/** 权限指令 **/
const has = {
  mounted(el: HTMLElement, binding: DirectiveBinding, vnode: any) {
    // 获取页面按钮权限
    let btnPermissionsArr: string[] = [];

    if (binding.value) {
      // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
      btnPermissionsArr = Array.of(binding.value);
    } else {
      // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
      btnPermissionsArr = vnode.appContext.config.globalProperties.$route.meta.btnPermissions;
    }

    if (!vnode.appContext.config.globalProperties.$_has(btnPermissionsArr)) {
      el.parentNode?.removeChild(el);
    }
  }
};

// 权限检查方法
const $_has = function (value: string[]): boolean {
  let isExist = false;
  // 获取用户按钮权限
  let btnPermissionsStr = sessionStorage.getItem('btnPermissions');
  if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    return false;
  }
  if (value.indexOf(btnPermissionsStr) > -1) {
    isExist = true;
  }
  return isExist;
};

export { has, $_has };

请注意以下几点:

  1. 使用 DirectiveBinding 来替代 Vue 2 中的 binding。
  2. 使用 vnode.appContext.config.globalProperties 来访问 Vue 实例中的全局属性,因为在 Vue 3 中 $root 被废弃。
  3. 使用 el.parentNode?.removeChild(el); 来处理可能为空的情况,因为 TypeScript 严格模式下要求对可能为 null 或 undefined 的值进行处理。
  4. 这里假定你的权限检查方法 $_has 已经在全局可用。这是 Vue 3 的一种全局方法的推荐做法。

在使用的按钮中只需要引用v-has指令

<el-button @click='editClick' type="primary" v-has>编辑</el-button>

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

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

相关文章

QProcess 启动 进程 传参数 启动控制台进程 传参

目录 QProcess 启动外部程序的两种方式 依赖式 分离式&#xff1a; 启动进程前的预处理 设置启动路径 设置启动命令参数 设置启动工作目录 设置启动所需环境&#xff1a; 启动的状态 code smple: QProcess 控制台进程 QProcess启动控制台不显示窗口 注意&#xff1a;…

jvm基本概念,运行的原理,架构图

文章目录 JVM(1) 基本概念:&#xff08;2&#xff09;运行过程 今天来和大家聊聊jvm&#xff0c; JVM (1) 基本概念: JVM 是可运行Java代码的假想计算机&#xff0c;包括一套字节码指令集、一组寄存器、一个栈一个垃圾回收&#xff0c;堆 和 一个存储方法域。JVM 是运行在操作…

9.ROS的TF坐标变换(三):坐标系关系查看与一个案例

1 查看目前的坐标系变化 我们先安装功能包&#xff1a; sudo apt install ros-melodic-tf2-tools安装成功&#xff01; 我们先启动上次的发布坐标变换的节点&#xff1a; liuhongweiliuhongwei-Legion-Y9000P-IRX8H:~/Desktop/final/my_catkin$ source devel/setup.bash liuho…

cyclictest 交叉编译与使用

目录 使用版本问题编译 numactl编译 cyclictest使用参考 cyclictest 主要是用于测试系统延时&#xff0c;进而判断系统的实时性 使用版本 rt-tests-2.6.tar.gz numactl v2.0.16 问题 编译时&#xff0c;需要先编译 numactl &#xff0c;不然会有以下报错&#xff1a; arm-…

Linux:优化原则

web系统的优化原则&#xff1a; 从单机到集群 对Linux系统自身的优化原则&#xff1a;

TCP报文解析

1.端口号 标记同一台计算机上的不同进程 源端口&#xff1a;占2个字节&#xff0c;源端口和IP的作用是标记报文的返回地址。 目的端口&#xff1a;占2个字节&#xff0c;指明接收方计算机上的应用程序接口。 TCP报头中的源端口号和目的端口号同IP报头中的源IP和目的IP唯一确定一…

【QT】Windows环境下,cmake引入QML

这里使用的QT库为5.7版本。 1、添加环境变量 QT库根目录环境变量 QTDIR QT库平台插件环境变量 QT_PLUGIN_PATH QML支持环境变量 QML2_IMPORT_PATH &#xff08;该环境变量仅在需要使用QML时添加&#xff09; QT库动态库环境变量&#xff0c;bin目录下包含了QT程序运行所需的dl…

电子学会C/C++编程等级考试2022年03月(四级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:拦截导弹 某国为了防御敌国的导弹袭击, 发展出一种导弹拦截系统。 但是这种导弹拦截系统有一个缺陷: 虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。 某天, 雷达捕捉到敌国的导弹来袭。…

第十一届蓝桥杯青少组省赛Python中高级组真题及赏析

练习最好的办法就是实战。拿真题来做&#xff0c;不是解析是赏析。带着欣赏的眼光看&#xff0c;题目不但不难&#xff0c;反倒增加不少乐趣。接下来揭开第十一届蓝桥杯青少组省赛python编程题的神秘面纱&#xff0c;我们来一一赏析&#xff0c;看难不难。 选择题 选择题都比较…

C++核心编程——类与对象基础

C核心编程——类与对象基础 类与对象封装构造函数普通构造拷贝构造初始化成员列表&#xff08;补充&#xff09; 析构函数对象数组对象指针指向对象的指针指向对象成员的指针this指针 静态成员静态数据成员静态成员函数 友元普通函数做友元函数友元成员函数友元类 类与对象 C面…

深度学习常见回归分支算法逐步分析,各种回归之间的优缺点,适用场景,举例演示

文章目录 1、线性回归&#xff08;Linear Regression&#xff09;1.1 优点1.2 缺点1.3 适用场景1.4 图例说明 2、多项式回归&#xff08;Polynomial Regression&#xff09;2.1 优点2.2 缺点2.3 适用场景2.4 图例说明 3、决策树回归&#xff08;Decision Tree Regression&#…

Linux基础命令(超全面,建议收藏!)

一、Linux的目录结构 /&#xff0c;根目录是最顶级的目录了 Linux只有一个顶级目录&#xff1a;/ 路径描述的层次关系同样使用/来表示 /home/itheima/a.txt&#xff0c;表示根目录下的home文件夹内有itheima文件夹&#xff0c;内有a.txt 二、Linux命令基础格式 无论是什么…

孩子都能学会的FPGA:第十八课——用FPGA实现定点数的除法

&#xff08;原创声明&#xff1a;该文是作者的原创&#xff0c;面向对象是FPGA入门者&#xff0c;后续会有进阶的高级教程。宗旨是让每个想做FPGA的人轻松入门&#xff0c;作者不光让大家知其然&#xff0c;还要让大家知其所以然&#xff01;每个工程作者都搭建了全自动化的仿…

vivado实现分析与收敛技巧6-策略建议

典型时序收敛策略需运行大量实现策略并选取其中最佳的策略以供在实验室内应用。 ML 策略同样可选 &#xff0c; 且只需您运行3 项策略即可达成类似的 QoR 收益。这些策略使用机器学习来检验布线后设计的各项功能特性 &#xff0c; 以便预测相同设计上不同策略的性能。在 repo…

树莓派4b安装ubuntu22和向日葵设置开机启动

树莓派4b安装ubuntu22和向日葵设置开机启动 使用树莓派烧录系统工具烧录ubuntu 在树莓派官网下载官方软件&#xff0c;安装完后运行 在软件上选择 选择ubuntu桌面或者server 根据自己需求选择&#xff0c;这里我选择22.04的系统 烧录好以后进入系统 安装向日葵 下载树莓…

Android实验:启动式service

目录 实验目的实验内容实验要求项目结构代码实现结果展示 实验目的 充分理解Service的作用&#xff0c;与Activity之间的区别&#xff0c;掌握Service的生命周期以及对应函数&#xff0c;了解Service的主线程性质&#xff1b;掌握主线程的界面刷新的设计原则&#xff0c;掌握启…

如何在WordPress中批量替换图片路径?

很多站长在使用WordPress博客或者搬家时&#xff0c;需要把WordPress文章中的图片路径进行替换来解决图片不显示的问题。总结一下WordPress图片路径批量替换的过程&#xff0c;方便有此类需求的站长们学习。 什么情况下批量替换图片路径 1、更换了网站域名 有许多网站建设初期…

一文了解工业互联网是什么,和传统互联网的区别有哪些

几个问题 工业互联网和传统互联网有什么区别 1 业务方面&#xff0c;传统的互联网企业更多是toC的业务&#xff0c;直接面对的是一个个的个体&#xff0c;而工业互联网离消费者更远一点&#xff0c;往往是toB或者toG的&#xff1b; 个人认为这也是最根本的区别&#xff0c;由…

什么是Daily Scrum?

Daily Scrum&#xff08;每日站会&#xff09;&#xff0c;Scrum Master要确保这个会在每天都会开。这个会的目的就是检查正在做的东西和方式是否有利于完成Sprint目的&#xff0c;并及时做出必要的调整。 每日站会一般只开15分钟&#xff0c;为了让事情更简单些&#xff0c;这…

网上选课系统源码(Java)

JavaWebjsp网上选课系统源码 运行示意图&#xff1a;