vue前端菜单权限控制

权限式对特定资源都得访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源

而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式的触发

  • 页面加载触发

  • 页面上的按钮点击触发

总的来说,所有的请求发起都触发自前端路由或视图

所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:

  • 路由方面,用户登录后只能看到自己有权限访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页

  • 视图方面,用户只能看到自己有权浏览的内容和有权操作的空格键

  • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

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

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

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

  • 路由权限:如果用户没有登录,手动在地址栏敲入登录后主界面的地址,则需要跳转到登录界面。如果用户已经登录,如果手动敲入非权限内的地址,则需要跳转404 界面。

接口权限

接口权限目前一般采用 jwt 的形式来验证,没有通过的话一般返回 401,跳转到登录页面重新进行登录

登陆后拿到 token ,将 token 存起来,通过 axios 请求拦截器进行拦截,每次请求的时候头部携带 token,这块需要配置axios的请求拦截器。

复制代码
axios.interceptors.request.use(config => {
  config.headers['token'] = cookie.get('token')
  return config
})
axios.interceptors.response.use(res=>{},{response}=>{
  if (response.data.code === 40099 || response.data.code === 40098) { //token 过期或者错误
    router.push('/login')
  }
})
路由权限控制

方案一

初始化即挂载全部路由,并且再路由上标记响应的权限信息,每次路由跳转前做校验

复制代码
const routerMap = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor'] // you can set roles in root nav
    },
    children: [{
      path: 'page',
      component: () => import('@/views/permission/page'),
      name: 'pagePermission',
      meta: {
        title: 'pagePermission',
        roles: ['admin'] // or you can only set roles in sub nav
      }
    }, {
      path: 'directive',
      component: () => import('@/views/permission/directive'),
      name: 'directivePermission',
      meta: {
        title: 'directivePermission'
          // if do not set roles, means: this page does not require permission
      }
    }]
  }]

这种方式存在以下四种缺点:

  • 加载所有的路由,如果路由很多,而用户并不似所有路由都有权限访问,对性能会有影响

  • 全局路由守卫里,每次路由跳转都要做权限判断

  • 菜单信息写死在前端,要改革显示文字或者权限信息,需要重新编译

  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而路由不一定作为菜单显示,还要多家字段进行标识

方案二

初始化的时候先挂载不需要权限控制的路由,比如登录页,404 等错误页,如果用户通过 url 进行强制访问,则会直接进入404,相当于从源头上做了控制

登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用 addRoutes 添加路由

复制代码
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration
// permission judge function
function hasPermission(roles, permissionRoles) {
    if (roles.indexOf('admin') >= 0) return true // admin permission passed
    directly
    if (!permissionRoles) return true
    return roles.some(role => permissionRoles.indexOf(role) >= 0)
}
const whiteList = ['/login', '/authredirect']// no redirect whitelist
router.beforeEach((to, from, next) => {
    NProgress.start() // start progress bar
    if (getToken()) { // determine if there has token
        /* has token*/
        if (to.path === '/login') {
            next({ path: '/' })
            NProgress.done() // if current page is dashboard will not trigger af
            terEach hook, so manually handle it
        } else {
            if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完 user_info 信息
                store.dispatch('GetUserInfo').then(res => { // 拉取 user_info
                    const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
                    store.dispatch('GenerateRoutes', { roles }).then(() => { // 
                        roles
                        router.addRoutes(store.getters.addRouters) // 动态添加可访问路由
                        next({ ...to, replace: true }) // hack方法,确保 addRoutes 已完成,set the replace: true so the navigation will not leave a history record
                    })
                }).catch((err) => {
                    store.dispatch('FedLogOut').then(() => {
                        Message.error(err || 'Verification failed, please login again')
                        next({ path: '/' })
                    })
                })
            } else {
                // 没有动态改变权限的需求可直接 next() ↓ 删除下方权限哦按段
                if (hasPermission(store.getters.roles, to.meta.roles)) {
                    next()
                } else {
                    next({ path: '/401', replace: true, query: { noGoBack: true }})
                }
                // 可删 ↑
            }
        }
    } else {
        /* has no token*/
        if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
            next()
        } else {
            next('/login') // 否则全部重定向到登录页
            NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
        }
    }
})
router.afterEach(() => {
    NProgress.done() // finish progress bar
})

按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

这种方式也存在了以下的缺点:

  • 全局路由守卫里,每次路由跳转都要做判断

  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译

  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,换要多加字段进行标识

菜单权限

菜单权限可以理解成将页面与理由进行解耦

方案一

菜单与路由分离,菜单由后端返回

前端定义路由

复制代码
{
 name: "login",
 path: "/login",
 component: () => import("@/pages/Login.vue")
}

name 字段不能为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有 name 对应的字段,并且唯一性校验

全局路由守卫里做判断

复制代码
function hasPermission(router, accessMenu) {
  if (whiteList.indexOf(router.path) !== -1) {
    return true;
  }
  let menu = Util.getMenuByName(router.name, accessMenu);
  if (menu.name) {
    return true;
  }
  return false;
}
Router.beforeEach(async (to, from, next) => {
  if (getToken()) {
    let userInfo = store.state.user.userInfo;
    if (!userInfo.name) {
      try {
        await store.dispatch("GetUserInfo")
        await store.dispatch('updateAccessMenu')
        if (to.path === '/login') {
          next({ name: 'home_index' })
        } else {
          //Util.toDefaultPage([...routers], to.name, router, next);
          next({ ...to, replace: true })// 菜单权限更新完成,重新近一次当前路由
        }
      } 
      catch (e) {
        if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
          next()
        } else {
          next('/login')
        }
      }
    } else {
      if (to.path === '/login') {
        next({ name: 'home_index' })
      } else {
        if (hasPermission(to, store.getters.accessMenu)) {
          Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
        } else {
          next({ path: '/403',replace:true })
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
      next()
    } else {
      next('/login')
    }
  }
  let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
  Util.title(menu.title);
});
Router.afterEach((to) => {
  window.scrollTo(0, 0);
})

每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的 name 与路由的 name 是一一对应的,而后端返回的菜单就已经是经过权限过滤的

如果根据路由 name 找不到对应的菜单,就表示用户没有权限访问

如果路由很多,可以在应用初始化的时候,之挂载不需要权限控制的路由,取得后端返回的菜单后,根据菜单与路由对应关系,筛选出可访问的路由,通过 addRoutes 动态挂载

缺点:

  • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用

  • 全局路由守卫里,每次路由跳转都要做判断

方案二

菜单和路由都由后端返回

前端同一定义路由组件

复制代码
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
 home: Home,
 userInfo: UserInfo
};
复制代码
后端路由组件返回格式如下
复制代码
[
 {
     name: "home",
     path: "/",
     component: "home"
 },
 {
     name: "home",
     path: "/userinfo",
     component: "userInfo"
 }
]

在将后端返回路由通过 addRoutes 动态挂载之间,需要将数据处理以下,将 component 字段换为真正的组件

如果有嵌套路由,后端功能涉及的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

缺点

  • 全局路由守卫里,每次路由跳转都要做判断

  • 前后端的配置要求更高

按钮权限

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

方案一

按钮权限也可以用 v-if 判断

但是如果页面过多,每个页面都要获取用户权限 role 和路由表里的 meta.btnPermissions 然后再做判断

方案二

通过自定义指令进行按钮权限判断

首先配置路由

复制代码
path: '/permission',
  component: Layout,
  name: ' ',
  meta: {
  btnPermissions: ['admin', 'supper', 'normal']
},
// 页面需要的权限
children: [{
  path: 'supper',
  component: _import('system/supper'),
  name: ' 测试页面',
  meta: {
    btnPermissions: ['admin', 'supper']
  } // 页面需要的权限
},
           {
             path: 'normal',
             component: _import('system/normal'),
             name: ' ',
             meta: {
               btnPermissions: ['admin']
             } // 页面需要的权限
           }]
}

自定义权限鉴定指令

复制代码
import Vue from 'vue'
/** 权限指令 **/
const has = Vue.directive('has', {
  bind: function (el, binding, vnode) {
    // 获取页面按钮权限
    let btnPermissionsArr = [];
    if(binding.value){
      // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较
      btnPermissionsArr = Array.of(binding.value);
    }else{
      // 否则获取路由中的参数,根据路由的 btnPermissionsArr 和当前登录人按钮权限做比较
      btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
    }
    if (!Vue.prototype.$_has(btnPermissionsArr)) {
      el.parentNode.removeChild(el);
    }
  }
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
  let isExist = false;
  // 获取用户按钮权限
  let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
  if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    return false;
  }
  if (value.indexOf(btnPermissionsStr) > -1) {
    isExist = true;
  }
  return isExist;
};
export {has}

再使用的按钮中只需要引用 v-has 指令

复制代码
<el-button @click='editClick' type="primary" v-has> </el-button>
小结
  • 关于权限如何选择那种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离

  • 权限需要前后端结合,前端尽可能的去控制,更多的需要后端判断

相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷6 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript