我也该去了解了解权限管理了

135 阅读5分钟

前言

大家好,我是啊肥,目前在尝试搭建自己的博客,做到权限这一块的时候,忽然想起似乎对这一块并没有那么熟悉,甚至对各种方案并不是很清晰,自己需要做什么,没有方向去考虑。伴着这些问题去查了资料,我也该了解了

权限管理是什么

简单来说就是对特定资源的访问许可,控制权限是确保某些用户能分配到某些资源

在前端方向中,一般去触发请求的是有页面加载触发和按钮交互触发

在这几方面入手,一般想实现几种效果:

  • 用户只能看到自己有权限的组件和内容
  • 用户只能访问自己有权限访问的菜单路由地址,否则跳转到提示页
  • 请求拦截,请求接口时会查看权限,在前端步骤进行拦截

怎么做权限管理

接口权限

普遍流程中,会存在token,如果请求中没有token会跳转登录页重新登录,拿到token会进行存储,通过axios请求拦截器进行拦截,每次请求头部都会携带token

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')
    }
})

按钮权限

这里都以vue来做实例

第一种:

页面通过api可获取路由里的meta,通过v-if判断

第二种

自定义vue指令判断

配置路由

{
    path: '/test',
    component: Test,
    name: '测试',
    meta: {
        btnPermissions: ['admin', 'normal']
    },
    //页面需要的权限
    children: [{
        path: 'childrenTest',
        component: _import('xxx'),
        name: 'children测试',
        meta: {
            btnPermissions: ['admin']
        } //页面需要的权限
    }]
}

自定义指令

import Vue from 'vue'
// 权限检查方法
Vue.prototype.$_pre = function (value) {
    let isExist = false;
    // 获取用户按钮权限
    let btnPermissionsStr = '缓存获取权限';
    if (!btnPermissionsStr) return false;
    if (value.indexOf(btnPermissionsStr) > -1) isExist = true;
    return isExist;
};
// 指令
const pre = Vue.directive('pre', {
    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.$_pre(btnPermissionsArr)) {
            el.parentNode.removeChild(el);
        }
    }
});
export {pre}

使用

<button v-pre>点击</button>

路由权限

第一种方案

初始化挂载全部的路由,每个路由都入上文,标记权限

{
    path: '/test',
    component: Test,
    name: '测试',
    meta: {
        roles: ['admin', 'normal']
    },
    //页面需要的权限
    children: [{
        path: 'childrenTest',
        component: _import('xxx'),
        name: 'children测试',
        meta: {
            roles: ['admin']
        } //页面需要的权限
    }]
}

缺点:

  • 全局路由守卫,每次跳转都要判断
  • 加载在所有的路由,如果路由较多,影响性能
  • 菜单写死前端,每次更改需要发布
  • 菜单跟路由耦合

第二种方案

初始化先挂载不需要权限的路由,比如登录和404错误页等等,如果用户通过url访问,会强制进入404页面,登录后获取权限,然后再去筛选对应的路由,通过addRoutes添加路由

import router from './router'

// 权限判断
function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true // 默认赋予admin权限
  if (!permissionRoles) return true
  return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/xxxx']// 白名单

router.beforeEach((to, from, next) => {
  if (如果有token) {
    if (to.path === '/login') next({ path: '/' })
    else {
      if (当前用户没有拉取完user信息) {
        获取用户信息.then(res => { // 拉取user
          const roles = res.data.roles
          根据roles生成可访问的路由fn(roles).then(() => {
            router.addRoutes(可访问路由表)
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch((err) => {
          next({ path: '/' })
        })
      } else {
        // 没有动态改变权限可直接next()
        if (hasPermission(可访问路由表, to.meta.roles)) next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) { // 免登录白名单,直接进入
      next()
    } else next('/login') // 重定向到登录页
  }
})

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

缺点:

  • 全局路由守卫,每次跳转都要判断
  • 菜单写死前端,每次更改需要发布
  • 菜单跟路由耦合

菜单权限 菜单与路由解耦

第一种方案

菜单由后端返回,前端定义路由信息

{
    name: 'xxx',
    path: 'xxx',
    component: () => import("xxxx/xx/xxx")
}

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

全局路由守卫

function hasPermission(router, accessMenu) {
  if (whiteList.indexOf(router.path) !== -1) return true;
  let menu = '获取匹配name的路由信息'
  if (menu.name) return true;
  return false;

}

Router.beforeEach(async (to, from, next) => {
  if (如果存在token) {
    let userInfo = '获取用户信息';
    if (!userInfo.name) {
      try {
        await 获取用户信息
        await 获取菜单
        if (to.path === '/login') next({ name: '首页' })
        else next({ ...to, replace: true }) // 菜单权限更新完成,重新进一次当前路由
      }  
      catch (e) {
        if (whiteList.indexOf(to.path) !== -1) next() // 在免登录白名单,如上文
        else next('/login')
      }
    } else {
      if (to.path === '/login') next({ name: '首页' });
      else {
        if (hasPermission(to, 菜单路由)) next() // 前往对应页面
        else next({ path: '/403',replace:true })
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) next() // 在免登录白名单,直接进入
    else next('/login')
  }
});

Router.afterEach((to) => {
  window.scrollTo(0, 0);
});

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

如果路由的name找不到对应的,会到404,无权访问

缺点:

  • 全局路由守卫,每次跳转都要判断
  • 菜单跟路由做一一对应,如果加新功能,则两边都要添加,保证正常使用

第二种方案

菜单和路由都由后端返回

前端

const Xx1 = () => import("xxxx");
const Xx2 = () => import("xxxx");
export default {
    xx1: Xx1,
    xx2: Xx2
};

后端

[
    {
        name: "xx1",
        path: "/xx1",
        component: "xx1"
    },
    {
        name: "xx2",
        path: "/xx2",
        component: "xx2"
    }
]

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

缺点:

  • 全局路由守卫,每次跳转都要判断
  • 更加要求前后端的配合