前端权限控制

1,478 阅读3分钟

前言

前端控制权限分为两部分,菜单页面 与 按钮。

菜单权限

一个是可见的菜单页面 :左侧dom节点,另外一个是- 一个是可访问的菜单页面 :系统当中路由这一块。

 路由权限

接口返回菜单表数据格式如下:

[   {     'name': '首页', //页面名     'code': 'index',      'icon': 'menu', //图标     'path': '/list',      'orders': 1,     'is_hide': 0,     'reserve': {},     'components': [], //这个存放按钮权限code 的信息
     'children': [ //子路由
       {
         'name': '页面1',
         'code': 'page1',
         'icon': 'menu',
         'path': '/list/page1',
         'orders': 2,
         'is_hide': 0,
         'components': [ //这个存放按钮权限code 的信息
           {
             'code': 'api/add', //按钮权限会用到
             'name': '新增',
             'orders': 1,
             'reserve': {}
           },
            {
             'code': 'api/update',
             'name': '修改',
             'orders': 1,
             'reserve': {}
           }
           ....
         ],
         'reserve': {
           componentName: 'Page1'
         }
       },
       {
         'name': '页面2',
         'code': 'page2',
         'icon': 'menu',
         'path': '/list/page2',
         'orders': 1,
         'is_hide': 0,
         'components': [
           {
             'code': 'api/query',
             'name': '菜单查询',
             'orders': 1,
             'reserve': {}
           }
         ],
         'reserve': {
           componentName: 'Page2'
         }
       }
     ]
   }
 ]

方案一

简单来说前端页面保存全部的路由配置,登录后获取用户的权限,然后过滤得到有权限的路由配置,动态添加到路由中。

router.config.js

/**
 * 基础路由 白名单
 * @type { *[] }
 */
export const constantRouterMap = [
  {
    path: '/login',
    component: Layout,
    redirect: '/user/login',
    hidden: true,
    children: [
      {
        path: 'login',
        name: 'login',
        component: () => import(/* webpackChunkName: "user" */ '@/views/Login')
      }
    ]
  },
  /**
   * 异常页
   */
  {
    path: '/404',
    name: '404',
    component: () => import(/* webpackChunkName: "fail" */ '@/views/Exception/404')
  }
   ...
]
export const routerMap = [
    {
    path: '/',
    name: 'index',
    redirect: '/main',
    component: BasicLayout,
    meta: { title: '首页' },
    children: [
       {
        path: '/ac',
        name: 'Main',
        component: () => import('@/views/Main'),
        meta: { title: '活动管理', keepAlive: true, icon: 'project' },
        children: [
          {
             path: '/ac/ac_config',
            name: 'ActivityConfiguration',
            component: () => import('@/views/Main/ActivityConfiguration'),
            meta: { title: '活动配置管理', keepAlive: true }
          }
          ...
        ]
      }
      ....
     ]
    }
]

登录之后请求获取菜单接口,获取有权限的菜单,收集路由code之后跳转到首页。router.js每次在路由跳转之前做过滤, 使用router.addRoute动态添加到路由中。

    /**
     * 收集路由code   
     * 登录接口成功之后调用 将routerCodeArr存起来
     */
    collectRouterCode (data) {
      for (const i of data) {
        if (i.code) {
          this.routerCodeArr[i.code] = {}
          if (i.components && i.components.length) {
            for (const j of i.components) {
              this.routerCodeArr[i.code][j.code] = true
            }
          }
        }
        if (i.children && i.children.length) {
          this.collectRouterCode(i.children)
        }
      }
    },

routerCodeArr:key是有权限的页面code,里面装的按钮权限

{
 "index":{},
 "page1": {
       "api/add":true, 
       "api/delete": true, 
 },
 "page2":{
       "api/query":true
  }
  ....
}

permission.js

// 路由白名单
const whiteList = ['login', '403', '404', '500']
router.beforeEach((to, from, next) => {
  NProgress.start()
  if (routerCodeArr && routerCodeArr) { //登录成功才有这个routerCodeArr
    if (store.state.login.addRouters.length === 0) {
      // 动态添加可访问路由表
      filterRouter(routerMap)
      //动态添加路由!
      router.addRoutes(routerMap)
      store.dispatch('set_add_routers', routerMap)
      const redirect = decodeURIComponent(from.query.redirect || to.path)
      if (to.path === redirect) {
        // hack方法 确保addRoutes已完成
        next({ ...to, replace: true })
      } else {
        // 跳转到目的路由
        next({ path: redirect })
      }
    } else {
    // 在免登录白名单,直接进入
    if (whiteList.includes(to.name)) {
      next()
    } else {
      next({ path: '/user/login', query: { redirect: to.fullPath } })
      NProgress.done()
      }
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

filterRouter 方法做过滤

/**
 * 过滤路由
 */
function filterRouter (data) {
  const accessedRouters = data.filter(route => {
    if (routerCodeArr[route.name]) {
      route.meta['auth'] = routerCodeArr[route.name]
      if (route.children && route.children.length) {
        route.children = filterAsyncRouter(route.children)
      }
      return true
    }
    return false
  })
  return accessedRouters
}

但是随着项目越来越大,会导致这个静态的路由配置也越来越长,不好维护。

方案二

简单来说就是根据后端返回的菜单列表,自己生成路由配置。

router.config.js 配置白名单

const constantRouter = [
 {
   path: '/login',
   component: () => import('@/views/login/index'),
   hidden: true
 },
 {
   path: '/404',
   component: () => import(/* webpackChunkName: "fail" */ '@/views/error/404')
 }
]

permission.js

router.beforeEach((to, from, next) => {
 NProgress.start()

 const hasToken = store.getters.token //表明已登录
 /* has token */
 if (hasToken) {
   if (to.path === '/login') {
     next()
     NProgress.done()
   } else {
     if (store.getters.routers.length === 0) {
     //生成路由配置
       store.dispatch('GenerateRoutes')
         .then((routers) => {
           //动态添加路由
           router.addRoutes(routers)
           const redirect = decodeURIComponent(from.query.redirect || to.path)
           if (to.path === redirect) {
             next({ ...to, replace: true })
           }
         }).catch((err) => {
           console.log(err)
           notification.error({
             message: '错误',
             description: '请求菜单信息失败,请重试'
           })
           store.dispatch('Logout')
         })
     }
     next()
   }
 } else {
   if (whiteList.includes(to.path)) {
     next()
   } else {
     next({ path: '/login', query: { redirect: to.fullPath } })
     NProgress.done()
   }
 }
})

router.afterEach(() => {
 NProgress.done()
})

GenerateRoutes 方法生成路由配置

    /**
     * 生成路由
     */
    GenerateRoutes ({ commit }) {
      return new Promise((resolve, reject) => {
        dynamicRouter().then(({ routers, menus }) => {
          commit('SET_ROUTERS', routers)
          resolve(routers)
        }).catch(error => {
          reject(error)
        })
      })
    }

dynamicRouter 获取路由菜单信息

/**
 * 获取路由菜单信息
 */
export default () => {
  return new Promise((resolve, reject) => {
        const routers = generator(menus)
        routers.unshift(indexRouter)
        asyncRouter[0].children = routers
        resolve({
          routers: asyncRouter,
          menus: res.data.menus
        })
  })
}

generator 生成路由表

/**
* 递归生成层级路由表
*/
const generator = routerMap => {
 return routerMap
   .map(item => {
     const currentRouter = {
       path: item.path,
       name: item.reserve.componentName,
       meta: {
         title: item.name,
         code: item.code,
         icon: item.icon,
         permissions: item.components.map(item => item.code), //收集路由code
         hidden: !!item.is_hide,
         ...item.reserve
       },
       component: ''
     }
     if (item.children && item.children.length > 0) {
       currentRouter.component = BlankLayout
       currentRouter.children = generator(item.children)
     } else {
       currentRouter.component = () => import(`@/views${item.path}/`)
     }

     return currentRouter
   })
   .filter(item => {
     return !(!item || item === '')
   })
}

按钮权限

按钮权限就是控制每个页面上面的按钮的显示。没有权限就不显示此按钮。

接口返回的菜单列表包含了每个页面拥有的按钮权限,收集完按钮权限code之后就通过指令来控制显示。

directive 指令定义

 */
import router from '@/router'
import Vue from 'vue'

const auth = Vue.directive('action', {
 inserted: function (el, binding, vnode) {
   const { value } = binding
   const curRouter = router.app.$route
   if (curRouter.meta.permissions.length && curRouter.meta.permissions.indexOf(value) > -1) {
   } else {
     el.parentNode & el.parentNode.removeChild(el) || (el.style.display = 'none')
   }
 }
})

export default auth

v-auth 使用

 <a-button  v-action="'api/add'">新增</a-button>