首页js教程 正文

Vue + ElementUI 手撸后台管理网站基本框架(二)权限控制

时间: 2021年8月30日 浏览 149

前端权限控制的本质
在管理系统中,感觉最让新手们搞不懂就是权限管理这部份了,而这也是后台管理系统中最基础的部分。
一般说来,权限管理可以分为两种情况:第一种为页面级访问权限,第二种为数据级操作权限。第一种情况是非常常见的,即用户是否能够看到页面;第二种情况是用户能否对数据进行增删改查等操作。
这两种情况在实际的前端表现中都是一致的,即用户在正常页面中看不见,看不见就不能操作,所以进行了一次视觉上的隔离。
在这里有的人可能会说:“你这不是自欺欺人吗?视觉隔离有什么用啊,有好多方法能够绕过去的啊,比如改改代码或者直接用ajax访问”。
是的,你说的对,这其实就是自欺欺人,不过这也正是我要说的:
前端的权限控制实质上就是用于展示,让操作变得更加友好,真正的安全实际上是由后端控制的
这里举个例子简单说明一下。如果用户通过其他方式绕过了前端的路由控制,访问到了该用户原本不能访问的页面,然后呢?页面中的数据需要从后端获取,页面中的操作也需要发送给后端,如果后端自身对于所有的请求接口有自己的权限控制,那么用户其实只能看到这个页面中固有的那些信息。
这里其实还有一个大家很容易想到的问题:在做接口测试时,如果系统本身有权限设置,但是接口未做权限,那么测试直接使用API测试工具访问的话…呵呵
所以说前端根本不用纠结于用户以各种形式绕过页面的问题,而以上这些问题在后端那里则根本不是问题。
权限策略
在理解了前端权限的本质后,我们说一下前端的权限策略。依照我目前的了解,大致上把权限策略分为以下两种:
  前端记录所有的权限。用户登录后,后端返回用户角色,前端根据角色自行分配页面
  前端仅记录页面,后端记录权限。用户登陆后,后端返回用户权限列表,前端根据该列表生成可访问页面
第一种的优点是前端完全控制,想怎么改就怎么改;缺点是当角色越来越多时,可能会给前端路由编写上带来一定的麻烦。
第二种的优点是前端完全基于后端数据,后期几乎没有维护成本;缺点是为了降低维护成本,必须拥有菜单配置页面及权限分配页面,否则就是噩梦。
本篇采用第二种方式,使用第一种方式的可以去看花裤衩的开源项目的教程:
手摸手,带你用vue撸后台 系列二(登录权限篇)
接下来,将一点一点带你实现前端权限控制。
接口权限控制
上文中说到,前端权限控制中,真正能实现安全的是接口,所以先实现接口的权限控制,而且这里听上去很重要,但实际上却很简单。
接口权限控制说白了就是对用户的校验。正常来说,在用户登录时服务器应给前台返回一个token,以后前台每次调用接口时都需要带上这个token,服务端获取到这个token后进行比对,如果通过则可以访问。
我们通过对axios进行简单的设置,增加请求拦截器,为每个请求的Header信息中增加Token。以下为伪代码:

const service = axios.create()

// http request 拦截器
// 每次请求都为http头增加Authorization字段,其内容为token
service.interceptors.request.use(
    config => {
        config.headers.Authorization = `${token}`
        return config
    }
);
export default service

这样简单的设置后即可实现在每次接口权限控制的前端部分。接下来则是分别讲述“页面级访问控制”和“数据级操作控制”的前端实现方式。如果对数据级操作控制不感兴趣的可以直接跳过。
页面级访问权限控制
虽然页面级访问控制实质上应该是控制页面是否显示,但落在实际中则有两种不同的情况:
  显示系统中所有菜单,当用户访问不在自己权限范围内的页面时提示权限不足。
  只显示当前用户能访问的菜单,如果用户通过URL进行强制访问,则会直接404。
至于第一种形式,个人认为在用户体验上非常不好,但不排除有某些特殊场景会用到这种方式。而本篇的权限控制是基于第二种形式。
依据刚才选定的方式,及上文中提到的权限策略,我们能够联想并梳理出一个大致的流程,这也是本篇中实际权限的流程:
  登录 ——> 获取该用户权限列表 ——> 根据权限列表生成能够访问的菜单 ——> 点击菜单,进入页面
在对流程梳理完成后我们开始进行详细的编写。
创建路由表
在创建路由表前,我们先总结一下前端路由表构成的两种模式:
  同时拥有静态路由和动态路由。
  只拥有静态路由
在第一种模式中,将系统中不需要权限的页面构成静态路由,需要权限的页面构成动态路由。当用户登录后,根据返回数据匹配动态路由表,将结果通过addRoutes方法添加到静态路由中。完整的路由中将只包含当前用户能访问的页面,用户无权访问的页面将直接跳转到404。(这也是我之前一直使用的模式)
第二种模式则直接将所有页面都配置到静态路由中。用户正常登录,系统将返回数据记录下来作为权限数据。当页面跳转时,判断跳转的页面是否在用户的权限列表中,如果在则正常跳转,如果不在则可以跳转到任意其他页面。
在经过不断的实践和改进后,个人认为第二种模式相对简单,只有单一的路由表,方便以后的管理和维护。同时又因无论哪种模式都不能避免所谓的“安全”问题——个别情况下跳过前端路由情况的发生,所以第二种模式无论怎么看都比第一种要好很多。
需要注意的是,在第二种模式中,因为只有单一的静态路由,所以一定要使用vue-router的懒加载策略对路由组件进行加载行为优化,防止首次加载时直接加载全部页面组件的尴尬问题。当然,你可以对那些不需要权限的固定页面不使用懒加载策略,这些页面包括登录页、注册页等。
  关于路由懒加载的知识也可以查看官方文档。:vue-router懒加载
路由表配置

const staticRoute = [
    {
        path: '/',  redirect: '/login'
    },
    {   path: '/login',
        component: () => import(/* webpackChunkName: 'index' */ '../page/login')
    },
    // 你的其他路由
    ...
    // 当页面地址和上面任一地址不匹配,则跳转到404
    { path: '*',
        redirect: '/404'
    }
]
export default staticRoute

Mock权限列表数据
在编写完路由表后,我们需要模拟一个完整的权限列表数据。这个数据在实际中是用户登录完成,后台返回的数据。这里暂定为以下格式:
模拟返回的权限列表数据

var data = [
    {   path: '/home',
        name: '首页'
    },
    {        name: '系统组件',
        child: [
            {  name: '介绍',
                path: '/components'
            },
            { name: '功能类',
                child: [
                    { path: '/components/permission',
                        name: '详细鉴权'
                    },
                    {  path: '/components/pageTable',
                        name: '表格分页'
                    }
                ]
            }
        ]
    }
]    

其中的name和path的值实际上应该在单独的菜单配置页面中填写,提交给后台,让其记录角色信息。
这里单独说一下我司现在的做法,以更好的说明该格式的实际使用环境:
  建立系统菜单。需要填写菜单的名称,和页面地址。多语言条件下填写多种名称。
  创建权限组。需要填写权限组名称,并为该权限组分配页面级权限及数据级权限。
  分配用户权限。将用户和某个权限组绑定。
以上配置均在前端有对应的页面进行操作。
编写导航钩子
上文中我们说过,需要在页面跳转时,将路由表和返回数据进行匹配,如果存在于返回数据中则正常跳转,如果不存在则跳转到任意页面。这里我们需要实现这个逻辑。以下为简单实现,只说明原理,不涉及细节。

// router为vue-router路由对象
router.beforeEach((to, from, next) => {
    // ajax获取权限列表函数
    // 这里省略了一些判断条件,比如判断是否已经拥有了权限数据等
    getPermission().then(res => {
        let isPermission = false
        permissionList.forEach((v) => {
            // 判断跳转的页面是否在权限列表中
            if(v.path == to.fullPath){
                isPermission = true
            }
        })
        // 没有权限时跳转到401页面
        if(!isPermission){
            next({path: "/error/401", replace: true})
        } else {
            next()
        }
    })
})

将数据放置到路由表中
为了实现“将数据放置到路由表中”这个功能,我们只需要将目光放回到上文中说的导航钩子那里。在那边,我们有一个getPermission()函数,用来获取权限。那么我们其实只要在这个函数的基础上进行修改,把返回的数据直接放到对应路由的meta字段中。
你可以戳这里查看路由meta字段的官方文档:路由元信息

// getPermission()
// ajax获取权限
axios.get("/permission").then((res) => {
    // 对返回数据扁平化,减少深层次循环
    let flatArr = flat(res)
    flatArr.forEach(function(v){
        let routeItem = router.match(v.path)
        if(routeItem){
            // 将返回的所有数据都存到路由的meta信息中
            routeItem.meta = v
        }
    })
})

根据路由表中的字段在页面中进行判断
实现该功能实际上就是实现一个类似于v-if判断的插件。当传入的参数存在于路由的meta字段时则渲染该节点,不存在时则不渲染。
这里期望的用法是这样的:

// 如果它有outport权限则显示
<el-button v-hasPermission="'outport'">导出</el-button>

而实现它则需要使用vue的自定义指令。你可以戳这里查看vue自定义指令的说明:自定义指令

const hasPermission = {
    install (Vue, options){
        Vue.directive('hasPermission', {
            bind(el, binding, vnode){
                let permissionList = vnode.context.$route.meta.permission
          if(permissionList && permissionList.length && !permissionList.includes(binding.value)){
                    el.parentNode.removeChild(el)
                }
            }
        })
    }
}

export default hasPermission

以上的方法便是对数据级权限(实质上是操作按钮)的显示控制。
路由控制完整流程图
至此为止,后台管理网站的权限控制流程就已经完全结束了,在最后我们总结一下完整的权限控制流程图。

NEXT——登录及系统菜单加载
在下一章中将重点讲述使用常规数据结构生成需要的菜单,以及unique-opened模式下点击跟节点不能收回多级菜单的问题
源码
当前源码地址:https://github.com/harsima/vue-backend