Vue 权限管理实现方案

实现 Vue 应用的路由、菜单、按钮级别权限控制

问题

在 Vue 应用中如何实现完整的权限管理系统,包括路由权限、菜单权限和按钮级别的权限控制?

解答

权限控制的本质

前端权限控制的核心是控制请求的发起权。请求触发来源有两种:页面加载触发和按钮点击触发。因此需要从路由和视图两方面入手控制。

接口权限

使用 JWT 进行接口权限验证,通过 axios 拦截器在每次请求时携带 token:

axios.interceptors.request.use(config => {
  const token = getToken()
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`
  }
  return config
}, error => {
  return Promise.reject(error)
})

axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response.status === 401) {
      // 跳转到登录页
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

路由权限

方案一:初始化挂载全部路由

在路由配置中标记权限信息,每次跳转前校验:

const routerMap = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true,
    meta: {
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor']
    },
    children: [...]
  }
]

router.beforeEach((to, from, next) => {
  const userRoles = store.getters.roles
  if (hasPermission(userRoles, to.meta.roles)) {
    next()
  } else {
    next('/401')
  }
})

缺点:加载所有路由影响性能,菜单信息写死在前端,路由与菜单耦合。

方案二:动态挂载路由

登录后根据权限动态添加路由:

router.beforeEach(async (to, from, next) => {
  const hasToken = getToken()
  
  if (hasToken) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // 获取用户权限信息
          const { roles } = await store.dispatch('user/getInfo')
          // 根据权限生成可访问路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // 动态添加路由
          router.addRoutes(accessRoutes)
          next({ ...to, replace: true })
        } catch (error) {
          await store.dispatch('user/resetToken')
          next(`/login?redirect=${to.path}`)
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

菜单权限

方案一:菜单与路由分离

前端定义路由,后端返回菜单,通过 name 字段关联:

// 前端路由定义
{
  name: "userManage",
  path: "/user/manage",
  component: () => import("@/pages/UserManage.vue")
}

// 后端返回菜单
[
  {
    name: "userManage",
    title: "用户管理",
    icon: "user"
  }
]

// 权限判断
function hasPermission(router, accessMenu) {
  if (whiteList.indexOf(router.path) !== -1) {
    return true
  }
  let menu = accessMenu.find(item => item.name === router.name)
  return !!menu
}

方案二:菜单和路由都由后端返回

前端定义路由组件映射表:

// 组件映射
const componentMap = {
  home: () => import("@/pages/Home.vue"),
  userInfo: () => import("@/pages/UserInfo.vue")
}

// 后端返回
[
  {
    name: "home",
    path: "/",
    component: "home"
  }
]

// 处理路由
function formatRoutes(routes) {
  return routes.map(route => {
    return {
      ...route,
      component: componentMap[route.component]
    }
  })
}

const accessRoutes = formatRoutes(backendRoutes)
router.addRoutes(accessRoutes)

按钮权限

使用自定义指令实现按钮级别权限控制:

// 路由配置
{
  path: '/permission',
  component: Layout,
  meta: {
    btnPermissions: ['edit', 'delete', 'add']
  }
}

// 自定义指令
import Vue from 'vue'

Vue.directive('has', {
  bind: function (el, binding, vnode) {
    let btnPermissionsArr = []
    
    if (binding.value) {
      // 使用指令传值
      btnPermissionsArr = Array.of(binding.value)
    } else {
      // 使用路由配置
      btnPermissionsArr = vnode.context.$route.meta.btnPermissions
    }
    
    if (!Vue.prototype.$_has(btnPermissionsArr)) {
      el.parentNode.removeChild(el)
    }
  }
})

// 权限检查方法
Vue.prototype.$_has = function (value) {
  const btnPermissionsStr = sessionStorage.getItem("btnPermissions")
  if (!btnPermissionsStr) {
    return false
  }
  return value.some(item => btnPermissionsStr.includes(item))
}

使用方式:

<template>
  <div>
    <el-button @click="editClick" type="primary" v-has="'edit'">编辑</el-button>
    <el-button @click="deleteClick" type="danger" v-has="'delete'">删除</el-button>
  </div>
</template>

关键点

  • 接口权限通过 JWT 和 axios 拦截器实现,401 状态码跳转登录页
  • 路由权限推荐动态挂载方案,登录后根据用户权限动态添加路由,避免加载无权限路由
  • 菜单权限可选择菜单与路由分离或完全由后端返回,根据项目复杂度选择合适方案
  • 按钮权限使用自定义指令 v-has 实现,通过移除 DOM 元素控制按钮显示
  • 前端权限控制是辅助手段,核心权限验证必须在后端完成