vue项目的菜单权限判断的实现

vue 专栏收录该内容
55 篇文章 1 订阅

在后台管理系统中,上一篇文章我们说到了按钮级别的权限判断,请看:GO,还有一个重要的权限判断就是菜单权限的判断,每个角色分给不同的可看菜单时必要做的事情。而且菜单全权限判断相比按钮权限判断会难实现的多。菜单权限的控制无非会有这几种思路:

        1.若是角色不多,根据角色分类,例如admin,编辑,游客这样划分的话,可直接根据人员绑定角色,在前端保存路由表,路由表中有权限字段,进入系统根据人员的所属角色去匹配前端保存的路由表里面的权限(admin,编辑还是游客),从而生成可看的路由表。这种方式只适合角色比较少的情况,因为角色是固定在前端定好的。

        2.第二种方式是在原来的基础上,前端只保存所有路由表,包括公共路由,异步路由,在系统我们可以选择用户所能看的菜单,然后保存权限数据在数据库。进入系统后根据前端保存的路由表与获取后端的权限数组进行匹配,从而生成可看的路由表。

        其中第二种方式勾选保存的菜单时,进入系统鉴权又有两种方式:1.直接只保存权限字段数组,用这个权限字段(菜单字段)去匹配前端路由表。2.保存所有菜单数据,包括path,component路径,title,icon等等,然后再自己组装路由表数据。这种方式组装路由表数据太麻烦了,特别是后台给的是一维数组,而不是已经有层级的树形数据时,而且我做的时候遇到组装component时菜单可显示,但是点击组件却跳转不了的问题,报component为undefined。最后果断放弃了,若有知道什么原因的小伙伴可留言探讨,多谢。

组装方式为:component:() => import(`@/page/${file}/${name}.vue`)

     做权限判断最有可能遇到的坑:死循环。当获取权限数据为空时,你保存在本地或者vuex中的权限数组就为空,所以当登入成功后要进入首页,判断vuex中的权限数据是否为空,为空则去拉取用户权限信息,获取的权限数据又为空,则如此死循环不停的去调接口拉取用户数据。所以在调试时一点要查看获取的权限数据,且给判断,若为空则特殊处理。

下面我们就从登入开始梳理菜单鉴权的所有逻辑思路,默认后端已经保存了权限数组(这个数组就是勾选的哪些菜单可看的字段,也可以说是菜单的name值),该数组我取的是每个路由表的name,因为保证这是唯一的即可。

login.vue:

调用登录接口,保存token,进入首页

 var md5Password = self.$md5(self.loginForm.password.toUpperCase())
          var para = {
            userName: self.loginForm.username,
            password: md5Password
          }
          var params = Qs.stringify(para)
          console.log('查询传入参数', para)
          login(params).then(res => {
            console.log('登录信息', res)
            if (res.token) {
              self.$store.dispatch('Settoken', res.token)
            } else {
              // 登入成功返回的数据中没有token
              self.$message.error('登录不成功,请重试...')
            }
            if (res.code === 1) {
              self.$message({
                type: 'success',
                message: '登入成功'
              })
              // 登入成功则进入首页
              self.$router.push({ path: '/dashboardPage/dashboard' })
            } else if (res.code === 0) {
              self.$message.error('登录不成功,请重试...')
            }
          })

 

permission.js:

在main.js中引入,菜单鉴权最重要的部分,在路由守卫中鉴权,包含了token鉴定,登入页面登入,地址栏输入地址是否可进的各种判断

import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 验权,从cookie中取得token

/** roleMenu:后台获取的菜单权限 ,Toname:要去路由的组件名称*/
function hasPermission(roleMenu, Toname) {
  var commonMenu = ['dashboardPage/dashboard', '/401', '/404', '/login'] // 定义公共页面数组
  if (roleMenu.indexOf(Toname) >= 0 || commonMenu.indexOf(Toname)) {
    // 若是公共页面数组或者在权限路由中,页面可进
    return true
  } else {
    return false
  }
}
const whiteList = ['/login'] // 不重定向白名单
router.beforeEach((to, from, next) => { // 注册一个路由前置守卫
  NProgress.start() // 进度条开始
  // 拉取本地token
  if (getToken()) {
    console.log('totototot', to)
    /* has token,有token*/
    if (to.path === '/login') {
      // 有token且进入的是登入页面,则直接进入首页
      next({ path: '/' })
      NProgress.done()
    } else {
      console.log('不是login进入')
      /* 有token不是login页面的情况(正常的路由切换),每次页面刷新清空vuex的roleMenu后都会重新去拉取权限数据,保证是最新的权限数据*/
      if (store.getters.roleMenu.length === 0) { // vuex中没有用户信息,手动输入url或者刷新浏览器,vuex数据失效情况,
        console.log('无用户信息进入')
        store.dispatch('GetInfoPower').then(res => { // 根据cookie的token调用接口拉取用户信息存储到vuex
          if (res !== -1 && res.length > 0) {
            // 实际项目拉取用户所拥有的权限
            var roleMenu = res
            store.dispatch('GetRouters', roleMenu).then(() => { // 根据roles权限生成可访问的路由表
              router.addRoutes(store.getters.lookRouter) // 动态添加可访问路由表
              next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
            })
          } else {
            // TOKEN过期
            store.dispatch('FedLogOut').then(() => {
              Message.error('用户信息已过期,请重新登入')
              next({ path: '/' })
            })
          }
        }).catch((err) => {
          // 拉取用户信息失败,重新登入
          console.log(err)
          store.dispatch('FedLogOut').then(() => {
            Message.error('获取用户信息失败,请重新登入')
            next({ path: '/' })
          })
        })
      } else {
        // 权限判断,防止在地址栏直接输入地址进入()
        if (hasPermission(store.getters.roleMenu, to.path)) { // vuex中有用户信息,且有权限
          next() // 有权限直接进入
        } else {
          // 无权限,进401页面
          next({ path: '401' })
        }
        // next()
      }
    }
  } else {
    console.log('无token进入')
    // 当cookie中没有token时
    if (whiteList.indexOf(to.path) !== -1) { // 当前页面为值得信任的页面免验证的页面则直接进入(要进入的页面能够在白名单中找到)
      next()
    } else {
      next({ path: '/login' })
      NProgress.done()
    }
  }
})
router.afterEach(() => { // 后置守卫,导航被确认
  NProgress.done() // 结束Progress
})

 vuex部分:

import { logout, getInfo } from '@/api/login'
import { setToken, removeToken, getToken } from '@/utils/auth'
import Qs from 'qs'

const user = {
  state: {
    token: getToken(),
    userName: '', // 用户名
    powerPort: [], // 权限接口
    roleMenu: [] // 后端返回的权限菜单字段数组集合

  },

  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_NAME: (state, userName) => { // 保存用户名
      state.userName = userName
    },
    SET_MENU: (state, roleMenu) => {
      state.roleMenu = roleMenu // 有权限路由菜单(动态添加该路由)
    },
    SET_PORT: (state, powerPort) => { // 保存可操作接口
      state.powerPort = powerPort
    }

  },

  actions: {
    // 保存token到vuex
    Settoken({ commit }, token) {
      setToken(token) // 存储调用API返回的token到cookie中
      commit('SET_TOKEN', token) // 存储token到vuex中
    },
    // 获取用户信息
    GetInfoPower({ commit, state }) {
      return new Promise((resolve, reject) => {
        var tokenObj = {
          token: state.token
        }
        var tokenPara = Qs.stringify(tokenObj)
        getInfo(tokenPara).then(response => {
          var data = response.rows
          console.log('权限数据', response)
          if (data !== undefined && data.length > 0) {
            commit('SET_NAME', response.userName)
            sessionStorage.setItem('userName', response.userName)
            var powerPort = [] // 可看操作接口
            var roleMenu = [] // 可看权限菜单字段
            // 遍历权限数组,只取code字段
            for (var i = 0; i < data.length; i++) {
              if (data[i].type === 0) {
                powerPort.push(data[i].code)
              } else {
                roleMenu.push(data[i].code)
              }
            }
            commit('SET_MENU', roleMenu)
            commit('SET_PORT', powerPort) // 保存获取数据库按钮权限数组到vuex
            sessionStorage.setItem('powerHandle', powerPort)
            resolve(roleMenu)
          } else {
            resolve(response.code)
          }
        }).catch(error => {
          reject(error)
        })
      })
    },
    // 登出
    LogOut({ commit, state }) {
      return new Promise((resolve, reject) => {
        console.log('登出', state.token)
        var tokenPara = {
          token: state.token
        }
        var tokenParams = Qs.stringify(tokenPara)
        logout(tokenParams).then(() => {
          commit('SET_TOKEN', '') // 退出登入时清空vuex中的token和roles
          commit('SET_MENU', [])
          commit('SET_NAME', '')
          console.log('登出')
          removeToken() // 退出登入移除token
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 前端 登出
    FedLogOut({ commit }) {
      return new Promise(resolve => {
        commit('SET_TOKEN', '')
        removeToken()
        console.log('前端登出')
        resolve()
      })
    },
  }
}

export default user

vuex中的permission.js:

根据获取的权限字段数组与本地的异步路由表进行匹配,生成可访问的路由表:

import { asyncRouterMap, constantRouterMap } from '@/router' // 引入权限路由和非权限路由

// 去前端保存的路由表中匹配路由
function findRouter(roles, route) {
  if (route.name) { // 若存在路由权限表,通过路由表中的name和后端保存的权限数组进行匹配判断
    return roles.indexOf(route.name) >= 0 // 检测roles中是否有在路由权限表中能找到的元素
  } else {
    return true
  }
}
// 过滤路由表
function filterAsyncRouterInmyRouter(asyncRouterMap, roles) {
  const accessedRouters = asyncRouterMap.filter(route => {
    if (findRouter(roles, route)) { // 存在路由权限
      if (route.children && route.children.length) { // 存在子路由
        route.children = filterAsyncRouterInmyRouter(route.children, roles)
      }
      return true
    }
    return false
  })
  return accessedRouters
}

const permission = {
  state: {
    lookRouter: [], // 可访问的权限路由
    AllRouter: constantRouterMap // 公共路由与异步权限路由拼接
  },
  mutations: {
    SET_LOOKROUTERS: (state, routers) => {
      state.lookRouter = routers // 可访问的权限路由
      state.AllRouter = constantRouterMap.concat(routers) // 公共路由与异步权限路由拼接
    }
  },
  actions: {
    GetRouters({ commit }, data) {
      return new Promise(resolve => {
        var accessedRouters = filterAsyncRouterInmyRouter(asyncRouterMap, data)
        var object = { path: '*', redirect: '/404', hidden: true }
        accessedRouters.push(object)
        commit('SET_LOOKROUTERS', accessedRouters) // 传入异步可访问的权限路由
        resolve()
      })
    }
  }
}

export default permission

 

vuex中的getter.js:

保存在vuexs的getter中,并

const getters = {
  token: state => state.user.token,
  userName: state => state.user.userName,
  roleMenu: state => state.user.roleMenu, // 权限菜单字段数组
  lookRouter: state => state.permission.lookRouter, // 异步权限路由数组(动态加载的路由)
  AllRouter: state => state.permission.AllRouter, // 公共页面的路由数

}
export default getters

vuex的store.js文件:

import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import getters from './getters''
import permission from './modules/permission'

Vue.use(Vuex)

const store = new Vuex.Store({
// 引入modules中的各个模块
  modules: {
    user,
    permission

  },
  getters // 引入getters中的计算属性模块
})

export default store

在侧边栏获取并渲染权限菜单:

mapGetters 映射到本地即可

<script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem'
import logo from './logo'
export default {
  components: { SidebarItem, logo },
  computed: {
    ...mapGetters([
      'sidebar', // 侧边栏状态及是否有动画
      'AllRouter' // 公共页面路由与异步权限路由拼接的总路由表
    ]),
    isCollapse() { // 侧边栏展开与否
      return !this.sidebar.opened
    }
  },
  created() {
    console.log('路由表', this.AllRouter)
  }
}
</script>

菜单鉴权从登陆,到路由守卫的鉴权,再到获取权限数据,保存到vuex,到从获取vuex中权限数据渲染在侧边栏,注释都非常清楚。若有什么需要改进或者错误的地方,欢迎指出,谢谢。

  • 2
    点赞
  • 0
    评论
  • 24
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值