附加 VUE项目权限管理详解

Vue权限控制

在Web系统中, 权限很久以来一直都只是后端程序所控制的.为什么呢?

  • 因为Web系统的本质围绕的是数据, 而和数据库最紧密接触的是后端程序.所以在很长的一段时间内, 权限一直都只是后端程序需要考虑的话题.但是随着前后端分离架构的流行, 越来越多的项目也在前端进行权限控制.

1.权限相关概念

1.1.权限的分类
  • 后端权限

    从根本上讲前端仅仅只是视图层的展示, 权限的核心是在于服务器中的数据变化, 所以后端才是权限的关键, 后端权限可以控制某个用户是否能够查询数据, 是否能够修改数据等操作

    • 后端如何知道该请求是哪个用户发过来的

      cookie
      session
      token
      
    • 后端的权限设计RBAC

      用户
      角色
      权限
      
  • 前端权限

    前端权限的控制本质上来说, 就是控制前端的 视图层的展示和前端所发送的请求. 但是只有前端权限控制没有后端权限控制是万万不可的. 前端权限控制只可以说是达到锦上添花的效果.

1.2.前端权限的意义

如果仅从能够修改服务器中数据库中的数据层面上讲,确实只在后端做控制就足够了, 那为什么越来越多的项目也进行了前端权限的控制, 主要有这几方面的好处

  • 降低非法操作的可能性

    不怕贼偷就怕贼惦记, 在页面中展示出一个 就算点击了也最终会失败 的按钮, 势必会增加有心者非法操作的可能性

  • 尽可能排除不必要请求,减轻服务器压力

    没必要的请求, 操作失败的请求, 不具备权限的请求, 应该压根就不需要发送, 请求少了, 自然也会减轻服务器的压力

  • 提高用户体验

    根据用户具备的权限为该用户展现自己权限范围内的内容,避免在界面上给用户带来困扰, 让用户专注于分内之事

2.前端权限控制思路

2.1.菜单的控制

在登录请求中, 会得到权限数据, 当然, 这个需要后端返回数据的支持. 前端根据权限数据, 展示对应的菜单.点击菜单,才能查看相关的界面.

2.2.界面的控制

如果用户没有登录,手动在地址栏敲入管理界面的地址, 则需要跳转到登录界面

如果用户已经登录, 可是手动敲入非权限内的地址, 则需要跳转404界面

2.3.按钮的控制

在某个菜单的界面中, 还得根据权限数据, 展示出可进行操作的按钮, 比如删除,修改,增加

2.4.请求和响应的控制

如果用户通过非常规操作, 比如通过浏览器调试工具将某些禁用的按钮变成启用状态, 此时发的请求, 也应该被前端所拦截

3. Vue的权限控制实现

3.1.菜单的控制
  • 查看登录之后获取到的数据

    {
    	"data": {
    		"id": 500,
    		"rid": 0,
    		"username": "admin",
    		"mobile": "13999999999",
    		"email": "123999@qq.com",
    		"token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM"
    	},
    	"rights": [{
    		"id": 125,
    		"authName": "用户管理",
    		"icon": "icon-user",
    		"children": [{
    			"id": 110,
    			"authName": "用户列表",
    			"path": "users",
    			"rights": ["view", "edit", "add", "delete"]
    		}]
    	 {
    		"id": 103,
    		"authName": "角色管理",
    		"icon": "icon-tijikongjian",
    		"children": [{
    			"id": 111,
    			"authName": "角色列表",
    			"path": "roles",
    			"rights": ["view", "edit", "add", "delete"]
    		}]
    	}, {
    		"id": 101,
    		"authName": "商品管理",
    		"icon": "icon-shangpin",
    		"children": [{
    			"id": 104,
    			"authName": "商品列表",
    			"path": "goods",
    			"rights": ["view", "edit", "add", "delete"]
    		}, {
    			"id": 121,
    			"authName": "商品分类",
    			"path": "categories",
    			"rights": ["view", "edit", "add", "delete"]
    		}]
    	}],
    	"meta": {
    		"msg": "登录成功",
    		"status": 200
    	}
    }
    

    在这部分数据中, 除了该用户的基本信息之外, 还有两个字段很关键

    • token,用于前端用户的状态保持
    • rights:该用户具备的权限数据,一级权限就对应一级菜单,二级权限就对应二级菜单
  • 根据rights中的数据, 动态渲染左侧菜单栏, 数据在Login.vue得到, 但是在Home.vue才使用, 所以可以把数据用vuex进行维护

    • vuex中的代码

      export default new Vuex.Store({
        state: {
          rightList:[]
        },
        mutations: {
          setRightList(state, data) {
            state.rightList = data
          }
        },
        actions: {
        },
        getters: {
        }
      })
      
    • Login.vue的代码

      login() {
          this.$refs.loginFormRef.validate(async valid => {
              ......
              this.$store.commit('setRightList', res.rights)
              this.$message.success('登录成功')
              this.$router.push('/home')
          })
      }
      
    • Home.vue的代码

      import { mapState } from 'vuex'
      computed: {
          ...mapState(['rightList'])
      }
      created() {
          this.activePath = window.sessionStorage.getItem('activePath')
          this.menulist = this.rightList
      },
      
  • 刷新界面菜单消失

    • 原因分析

      因为菜单数据是登录之后才获取到的, 获取菜单数据之后,就存放在Vuex中
      一旦刷新界面, Vuex中的数据会重新初始化, 所以会变成空的数组
      因此, 需要将权限数据存储在sessionStorage中, 并让其和Vuex中的数据保持同步
      
    • 代码解决

      export default new Vuex.Store({
        state: {
          rightList:JSON.parse(sessionStorage.getItem('rightList')||'[]')
        },
        mutations: {
          setRightList(state, data) {
            state.rightList = data
            sessionStorage.setItem('rightList',JSON.stringify(data))
          }
        },
        actions: {
        },
        getters: { 
        }
      })
      
  • 标识用户名, 方便查看当前用户

    • vuex的代码

      export default new Vuex.Store({
        state: {
          rightList:JSON.parse(sessionStorage.getItem('rightList')||'[]'),
          username: sessionStorage.getItem('username')
        },
        mutations: {
          setRightList(state, data) {
            state.rightList = data
            sessionStorage.setItem('rightList',JSON.stringify(data))
          },
          setUsername(state, data) {
            state.username = data
            sessionStorage.setItem('username',data)
          }
        },
        actions: {
        },
        getters: {
        }
      })
      
    • Login.vue的代码

      login() {
            this.$refs.loginFormRef.validate(async valid => {
              ......
              this.$store.commit('setRightList', res.rights)
              this.$store.commit('setUsername', res.data.username)
              this.$message.success('登录成功')
              this.$router.push('/home')
            })
          }
      
    • Home.vue的代码

      computed: {
          ...mapState(['rightList','username'])
      }
      <el-button type="info" @click="logout">{{username}}退出</el-button>
      
  • 退出按钮的逻辑

    logout() {
        sessionStorage.clear()
        this.$router.push('/login')
        window.location.reload()
    },
    
3.2.界面的控制

1.正常的逻辑是通过登录界面, 登录成功之后跳转到管理平台界面, 但是如果用户直接敲入管理平台的地址, 也是可以跳过登录的步骤.所以应该在某个时机判断用户是否登录

  • 如何判断是否登录

    sessionStorage.setItem('token', res.data.token)
    
  • 什么时机

    • 路由导航守卫

      router.beforeEach((to, from, next) => {
        if (to.path === '/login') {
          next()
        } else {
          const token = sessionStorage.getItem('token')
          if(!token) {
            next('/login')
          } else {
            next()
          }
        }
      })
      

2.虽然菜单项已经被控制住了, 但是路由信息还是完整的存在于浏览器,正比如zhangsan这个用户并不具备角色这个菜单, 但是他如果自己在地址栏中敲入/roles的地址, 依然也可以访问角色界面

  • 路由导航守卫

    路由导航守卫固然可以在每次路由地址发生变化的时候, 从vuex中取出rightList判断用户将要访问的界面, 这个用户到底有没有权限.不过从另外一个角度来说,这个用户不具备权限的路由, 是否也应该压根就不存在呢?

  • 动态路由

    • 登录成功之后动态添加

    • App.vue中添加

    • 代码如下:

      • router.js

        import Vue from 'vue'
        import Router from 'vue-router'
        import Login from '@/components/Login.vue'
        import Home from '@/components/Home.vue'
        import Welcome from '@/components/Welcome.vue'
        import Users from '@/components/user/Users.vue'
        import Roles from '@/components/role/Roles.vue'
        import GoodsCate from '@/components/goods/GoodsCate.vue'
        import GoodsList from '@/components/goods/GoodsList.vue'
        import NotFound from '@/components/NotFound.vue'
        import store from '@/store'
        
        Vue.use(Router)
        
        const userRule = { path: '/users', component: Users }
        const roleRule = { path: '/roles', component: Roles }
        const goodsRule = { path: '/goods', component: GoodsList }
        const categoryRule = { path: '/categories', component: GoodsCate }
        
        const ruleMapping = {
          'users': userRule,
          'roles': roleRule,
          'goods': goodsRule,
          'categories': categoryRule
        }
        
        const router = new Router({
          routes: [
            { 
              path: '/', 
              redirect: '/home' 
            },
            { 
              path: '/login', 
              component: Login 
            },
            {
              path: '/home',
              component: Home,
              redirect: '/welcome',
              children: [
                { path: '/welcome', component: Welcome },
                // { path: '/users', component: Users },
                // { path: '/roles', component: Roles },
                // { path: '/goods', component: GoodsList },
                // { path: '/categories', component: GoodsCate }
              ]
            },
            {
              path: '*',
              component: NotFound
            }
          ]
        })
        
        router.beforeEach((to, from, next) => {
          if (to.path === '/login') {
            next()
          } else {
            const token = sessionStorage.getItem('token')
            if(!token) {
              next('/login')
            } else {
              next()
            }
          }
        })
        
        export function initDynamicRoutes() {
          const currentRoutes = router.options.routes
          const rightList = store.state.rightList
          rightList.forEach(item => {
            item.children.forEach(item => {
              currentRoutes[2].children.push(ruleMapping[item.path])
            })
          })
          router.addRoutes(currentRoutes)
        }
        
        export default router
        
        
      • Login.vue

        import { initDynamicRoutes } from '@/router.js'
        login() {
              this.$refs.loginFormRef.validate(async valid => {
                if (!valid) return
                const { data: res } = await this.$http.post('login', this.loginForm)
                if (res.meta.status !== 200) return this.$message.error('登录失败!')
                this.$store.commit('setRightList', res.rights)
                this.$store.commit('setUsername', res.data.username)
                sessionStorage.setItem('token', res.data.token)
                initDynamicRoutes()
                this.$message.success('登录成功')
                this.$router.push('/home')
              })
            }
        
      • App.vue

        import { initDynamicRoutes } from '@/router.js'
        export default {
          name: 'app',
          created() {
            initDynamicRoutes()
          }
        }
        
3.3.按钮的控制

按钮控制

虽然用户可以看到某些界面了, 但是这个界面的一些按钮,该用户可能是没有权限的.因此, 我们需要对组件中的一些按钮进行控制. 用户不具备权限的按钮就隐藏或者禁用, 而在这块中, 可以把该逻辑放到自定义指令中

  • permission.js

    import Vue from 'vue'
    import router from '@/router.js'
    Vue.directive('permission', {
      inserted: function(el, binding){
        const action = binding.value.action
        const currentRight = router.currentRoute.meta
        if(currentRight) {
          if(currentRight.indexOf(action) == -1) {
            // 不具备权限
            const type = binding.value.effect
            if(type === 'disabled') {
              el.disabled = true
              el.classList.add('is-disabled')
            } else {
              el.parentNode.removeChild(el)
            }
          } 
        }
      }
    })
    
  • main.js

    import './utils/permission.js'
    
  • router.js

    export function initDynamicRoutes() {
      const currentRoutes = router.options.routes
      const rightList = store.state.rightList
      rightList.forEach(item => {
        item.children.forEach(item => {
          const itemRule = ruleMapping[item.path]
          itemRule.meta = item.rights
          currentRoutes[2].children.push(itemRule)
        })
      })
      router.addRoutes(currentRoutes)
    }
    
  • 使用指令

    v-permission="{action:'add'}"
    v-permission="{action:'delete', effect:'disabled'}"
    
3.4.请求和响应的控制
请求控制
  • 除了登录请求都得要带上token, 这样服务器才可以鉴别你的身份

    axios.interceptors.request.use(function(req){
      const currentUrl = req.url
      if(currentUrl !== 'login') {
        req.headers.Authorization = sessionStorage.getItem('token')
      }
      return req
    })
    
  • 如果发出了非权限内的请求, 应该直接在前端访问内组织,虽然这个请求发到服务器也会被拒绝

    import axios from 'axios'
    import Vue from 'vue'
    import router from '../router'
    // 配置请求的跟路径, 目前用mock模拟数据, 所以暂时把这一项注释起来
    // axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
    const actionMapping = {
      get: 'view',
      post: 'add',
      put: 'edit',
      delete: 'delete'
    }
    axios.interceptors.request.use(function(req){
      const currentUrl = req.url
      if(currentUrl !== 'login') {
        req.headers.Authorization = sessionStorage.getItem('token')
        // 当前模块中具备的权限
        // 查看  get请求  
        // 增加  post请求  
        // 修改  put请求   
        // 删除  delete请求 
        const method = req.method
        // 根据请求, 得到是哪种操作
        const action = actionMapping[method]
        // 判断action是否存在当前路由的权限中
        const rights = router.currentRoute.meta
        if(rights && rights.indexOf(action) == -1) {
          // 没有权限
          alert('没有权限')
          return Promise.reject(new Error('没有权限'))
        }
      }
      return req
    })
    axios.interceptors.response.use(function(res){
      return res
    })
    Vue.prototype.$http = axios
    
响应控制
  • 得到了服务器返回的状态码401, 代表token超时或者被篡改了, 此时应该强制跳转到登录界面

    axios.interceptors.response.use(function(res){
      if (res.data.meta.status === 401) {
        router.push('/login')
        sessionStorage.clear()
        window.location.reload()
      }
      return res
    })
    

4.小结

前端权限的实现必须要后端提供数据支持, 否则无法实现.

返回的权限数据的结构,前后端需要沟通协商, 怎样的数据使用起来才最方便.

4.1.菜单控制
  • 权限的数据需要在多组件之间共享, 因此采用vuex
  • 防止刷新界面,权限数据丢失, 所以需要存储在sessionStorage, 并且要保证两者的同步
4.2.界面控制
  • 路由的导航守卫可以防止跳过登录界面
  • 动态路由可以让不具备权限的界面的路由规则压根就不存在
4.3.按钮控制
  • 路由规则中可以增加路由元数据meta
  • 通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据
  • 自定义指令可以很方便的实现按钮控制
4.4.请求和响应控制
  • 请求拦截器和响应拦截器的使用
  • 请求方式的约定restful
  • 9
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值