Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 1320|回复: 0

Vue2.0用户权限控制解决方案

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-4-15 11:34:55 | 显示全部楼层 |阅读模式

    原文作者: 雅X共赏 

    原文地址:https://refined-x.com/2017/11/28/Vue2.0%E7%94%A8%E6%88%B7%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/

     

    Vue-Access-Control是一套基于Vue/Vue-Router/axios 实现的前端用户权限控制解决方案,通过对路由、视图、请求三个层面的控制,使开发者可以实现任意颗粒度的用户权限控制。

    安装

    版本要求

    • Vue 2.0x
    • Vue-router 3.x

    获取

    项目主页:https//refined-x.com/Vue-Access-Control/

    git:git clone https://github.com/tower1229/Vue-Access-Control.git

    运行

    1
    2
    3
    4
    5
    //开发
    npm run serve

    //构建
    npm build

    概述

    整体思路

    会话开始之初,先初始化一个只有登录路由的Vue实例,在根组件created钩子里将路由定向到登录页,用户登录成功后前端拿到用户token,设置axios实例统一为请求headers添加{"Authorization":token}实现用户鉴权,然后获取当前用户的权限数据,主要包括路由权限和资源权限,之后动态添加路由,生成菜单,实现权限指令和全局权限验证方法,并为axios实例添加请求拦截器,至此完成权限控制初始化。动态加载路由后,路由组件将随之加载并渲染,而后展现前端界面。

    为解决浏览器刷新路由重置的问题,拿到token后要将其保存到sessionStorage,根组件的created钩子负责检查本地是否已有token,如果有则无需登录直接用该token获取权限并初始化,如果token有效且当前路由有权访问,将加载路由组件并正确展现;若当前路由无权访问将按路由设置跳转404;如果token失效,后端应返回4xx状态码,前端统一为axios实例添加错误拦截器,遇到4xx状态码执行退出操作,清除sessionStorage数据并跳转到登录页,让用户重新登录。

    最小依赖原则

    Vue-Access-Control的定位是单一领域解决方案,除了Vue/Vue-Router/axios之外没有其他依赖,理论上可以无障碍的应用到任何有权限控制需求的Vue项目中,项目基于webpack 模板开发构建,大多数新项目可以直接基于检出代码继续开发。需要说明的是,项目额外引入的Element-UICryptoJS仅用于开发演示界面,他们不是必须且与权限控制毫无关系,项目应用中可以自行取舍。

    目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    src/
    |-- api/ //接口文件
    | |-- index.js //输出通用axios实例
    | |-- account.js //按业务模块组织的接口文件,所有接口都引用./index提供的axios实例
    |-- assets/
    |-- components/
    |-- router/
    | |-- fullpath.js //完整路由数据,用于匹配用户的路由权限得到实际路由
    | `-- index.js //输出基础路由实例
    |-- views/
    |-- App.vue
    ·-- main.js

    数据格式约定

    • 路由权限数据必须是如下格式的对象数组,idparent_id相同的两个路由具有上下级关系,如果希望使用自定义格式的路由数据,需要修改路由控制的相关实现,详见路由控制

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      [
      {
      "id": "1",
      "name": "菜单1",
      "parent_id": null,
      "route": "route1"
      },
      {
      "id": "2",
      "name": "菜单1-1",
      "parent_id": "1",
      "route": "route2"
      }
      ]
    • 资源权限数据必须是如下格式的对象数组,每个对象代表一个RESTful请求,支持带参数的url,具体格式说明见请求控制

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
       [
      {
      "id": "2c9180895e172348015e1740805d000d",
      "name": "账号-获取",
      "url": "/accounts",
      "method": "GET"
      },
      {
      "id": "2c9180895e172348015e1740c30f000e",
      "name": "账号-删除",
      "url": "/account/**",
      "method": "DELETE"
      }
      ]

    路由控制

    路由控制包括动态注册路由和动态生成菜单两部分。

    动态注册路由

    最初实例化的路由仅包括登录和404两个路径,我们期待完整的路由是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    [{
    path: '/login',
    name: 'login',
    component: (resolve) => require(['../views/login.vue'], resolve)
    }, {
    path: '/404',
    name: '404',
    component: (resolve) => require(['../views/common/404.vue'], resolve)
    }, {
    path: '/',
    name: '首页',
    component: (resolve) => require(['../views/index.vue'], resolve),
    children: [{
    path: '/route1',
    name: '栏目1',
    meta: {
    icon: 'icon-channel1'
    },
    component: (resolve) => require(['../views/view1.vue'], resolve)
    }, {
    path: '/route2',
    name: '栏目2',
    meta: {
    icon: 'ico-channel2'
    },
    component: (resolve) => require(['../views/view2.vue'], resolve),
    children: [{
    path: 'child2-1',
    name: '子栏目2-1',
    meta: {

    },
    component: (resolve) => require(['../views/route2-1.vue'], resolve)
    }]
    }]
    }, {
    path: '*',
    redirect: '/404'
    }]

    那么接下来就需要获取首页以及其子路由们,思路是事先在本地存一份整个项目的完整路由数据,然后根据用户权限对完整路由进行筛选。

    筛选的实现思路是先将后端返回的路由数据处理成如下哈希结构:

    1
    2
    3
    4
    5
    6
    7
    let hashMenus = {
    "/route1":true,
    "/route1/route1-1":true,
    "/route1/route1-2":true,
    "/route2":true,
    ...
    }

    然后遍历本地完整路由,在循环中将路径拼接成上述结构中的key格式,通过hashMenus[route]就可以判断路由是否匹配,具体实现见App.vue文件中的getRoutes()方法。

    如果后端返回的路由权限数据与约定不同,就需要自行实现筛选逻辑,只要能得到实际可用的路由数据就可以,最终使用addRoutes()方法将他们动态添加到路由实例中,注意404页面的模糊匹配一定要放在最后。

    动态菜单

    路由数据可以直接用来生成导航菜单,但路由数据是在根组件中得到的,导航菜单存在于index.vue组件中,显然我们需要通过某种方式共享菜单数据,方法有很多,一般来说首先想到的是Vuex,但菜单数据在整个用户会话过程中不会发生改变,这并不是Vuex的最佳使用场景,而且为了尽量减少不必要的依赖,这里用了最简单直接的方法,把菜单数据挂在根组件data.menuData上,在首页里用this.$parent.menuData获取。

    另外,导航菜单很可能会有添加栏目图标的需求,这可以通过在路由中添加meta数据实现,例如将图标class或unicode存到路由meta里,模板中就可以访问到meta数据,用来生成图标标签。

    在多角色系统中可能遇到的一个问题是,不同角色有一个名字相同但功能不同的路由,比如说系统管理员企业管理员都有”账号管理”这个路由,但他们的操作权限和目标不同,实际上是两个完全不同的界面,而Vue不允许多个路由同名,因此路由的name必须做区分,但把区分后的name显示在前端菜单上会很不美观,为了让不同角色可以享有同一个菜单名称,我们只要将这两个路由的meta.name都设置成”账号管理”,在模板循环时优先使用meta.name就可以了。

    菜单的具体实现可以参考views/index.vue

    视图控制

    视图控制的目标是根据当前用户权限决定界面元素显示与否,典型场景是对各种操作按钮的显示控制。实现视图控制的本质是实现一个权限验证方法,输入请求权限,输出是否获准。然后配合v-ifjsx或自定义指令就能灵活实现各种视图控制。

    全局验证方法

    验证方法的的实现本身很简单,无非是根据后端给出的资源权限做判断,重点在于优化方法的输入输出,提升易用性,经过实践总结最终使用的方案是,将权限跟请求同时维护,验证方法接收请求对象数组为参数,返回是否具有权限的布尔值。

    请求对象格式:

    1
    2
    3
    4
    5
    6
    7
    //获取账户列表
    const request = {
    p: ['get,/accounts'],
    r: params => {
    return instance.get(`/accounts`, {params})
    }
    }

    权限验证方法$_has()的调用格式:

    1
    v-if="$_has([request])"

    权限验证方法的具体实现见App.vueVue.prototype.$_has方法。

    将权限验证方法全局混入,就可以在项目中很容易的配合v-if实现元素显示控制,这种方式的优点在于灵活,除了可以校验权限外,还可以在判断表达式中加入运行时状态做更多样性的判断,而且可以充分利用v-if响应数据变化的特点,实现动态视图控制。

    具体实现细节参考基于Vue实现后台系统权限控制中的相关章节。

    自定义指令

    v-if的响应特性是把双刃剑,因为判断表达式在运行过程中会频繁触发,但实际上在一个用户会话周期内其权限并不会发生变化,因此如果只需要校验权限的话,用v-if会产生大量不必要的运算,这种情况只需在视图载入时校验一次即可,可以通过自定义指令实现:

    1
    2
    3
    4
    5
    6
    7
    8
    //权限指令
    Vue.directive('has', {
    bind: function(el, binding) {
    if (!Vue.prototype.$_has(binding.value)) {
    el.parentNode.removeChild(el);
    }
    }
    });

    自定义指令内部仍然是调用全局验证方法,但优点在于只会在元素初始化时执行一次,多数情况下都应该使用自定义指令实现视图控制。

    请求控制

    请求控制是利用axios拦截器实现的,目的是将越权请求在前端拦截掉,原理是在请求拦截器中判断本次请求是否符合用户权限,以决定是否拦截。

    普通请求的判断很容易,遍历后端返回的的资源权限格式,直接判断request.methodrequest.url是否吻合就可以了,对于带参数的url需要使用通配符,这里需要根据项目需求前后端协商一致,约定好通配符格式后,拦截器中要先将带参数的url处理成约定格式,再判断权限,方案中已经实现了以下两种通配符格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    1. 格式:/resources/:id
    示例:/resources/1
    url: /resources/**
    解释:一个名词后跟一个参数,参数通常表示名词的id

    2. 格式:/store/:id/member
    示例:/store/1/member
    url:/store/*/member
    解释:两个名词之间夹带一个参数,参数通常表示第一个名词的id

    对于第一种格式需要注意的是,如果你要发起一个url为"/aaa/bbb"的请求,默认会被处理成"/aaa/**"进行权限校验,如果这里的”bbb”并不是参数而是url的一部分,那么你需要将url改成"/aaa/bbb/",在最后加一个”/“表示该url不需要转化格式。

    拦截器的具体实现见App.vue中的setInterceptor()方法。

    如果你的项目还需要其他的通配符格式,只需要在拦截器中实现对应的检测和转化方法就可以了。

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-1-24 17:52 , Processed in 0.055455 second(s), 27 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表