前端权限控制的本质


在管理系统中,感觉最让新手们搞不懂就是权限管理这部份了,而这也是后台管理系统中最基础的部分。

一般说来,权限管理可以分为两种情况:第一种为页面级访问权限,第二种为数据级操作权限。第一种情况是非常常见的,即用户是否能够看到页面;第二种情况是用户能否对数据进行增删改查等操作。

这两种情况在实际的前端表现中都是一致的,即用户在正常页面中看不见,看不见就不能操作,所以进行了一次视觉上的隔离。

在这里有的人可能会说:“你这不是自欺欺人吗?视觉隔离有什么用啊,有好多方法能够绕过去的啊,比如改改代码或者直接用ajax访问”。

是的,你说的对,这其实就是自欺欺人,不过这也正是我要说的:

前端的权限控制实质上就是用于展示,让操作变得更加友好,真正的安全实际上是由后端控制的

这里举个例子简单说明一下。如果用户通过其他方式绕过了前端的路由控制,访问到了该用户原本不能访问的页面,然后呢?页面中的数据需要从后端获取,页面中的操作也需要发送给后端,如果后端自身对于所有的请求接口有自己的权限控制,那么用户其实只能看到这个页面中固有的那些信息。

这里其实还有一个大家很容易想到的问题:在做接口测试时,如果系统本身有权限设置,但是接口未做权限,那么测试直接使用API测试工具访问的话…呵呵

所以说前端根本不用纠结于用户以各种形式绕过页面的问题,而以上这些问题在后端那里则根本不是问题。

权限策略


在理解了前端权限的本质后,我们说一下前端的权限策略。依照我目前的了解,大致上把权限策略分为以下两种:

  1. 前端记录所有的权限。用户登录后,后端返回用户角色,前端根据角色自行分配页面
  2. 前端仅记录页面,后端记录权限。用户登陆后,后端返回用户权限列表,前端根据该列表生成可访问页面

第一种的优点是前端完全控制,想怎么改就怎么改;缺点是当角色越来越多时,可能会给前端路由编写上带来一定的麻烦。

第二种的优点是前端完全基于后端数据,后期几乎没有维护成本;缺点是为了降低维护成本,必须拥有菜单配置页面及权限分配页面,否则就是噩梦。

本篇采用第二种方式,使用第一种方式的可以去看花裤衩的开源项目的教程:
手摸手,带你用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

这样简单的设置后即可实现在每次接口权限控制的前端部分。接下来则是分别讲述“页面级访问控制”和“数据级操作控制”的前端实现方式。如果对数据级操作控制不感兴趣的可以直接跳过。

页面级访问权限控制


虽然页面级访问控制实质上应该是控制页面是否显示,但落在实际中则有两种不同的情况:

  1. 显示系统中所有菜单,当用户访问不在自己权限范围内的页面时提示权限不足。
  2. 只显示当前用户能访问的菜单,如果用户通过URL进行强制访问,则会直接404。

至于第一种形式,个人认为在用户体验上非常不好,但不排除有某些特殊场景会用到这种方式。而本篇的权限控制是基于第二种形式。

依据刚才选定的方式,及上文中提到的权限策略,我们能够联想并梳理出一个大致的流程,这也是本篇中实际权限的流程:

登录 ——> 获取该用户权限列表 ——> 根据权限列表生成能够访问的菜单 ——> 点击菜单,进入页面

在对流程梳理完成后我们开始进行详细的编写。

创建路由表

在创建路由表前,我们先总结一下前端路由表构成的两种模式:

  1. 同时拥有静态路由和动态路由。
  2. 只拥有静态路由

在第一种模式中,将系统中不需要权限的页面构成静态路由,需要权限的页面构成动态路由。当用户登录后,根据返回数据匹配动态路由表,将结果通过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的值实际上应该在单独的菜单配置页面中填写,提交给后台,让其记录角色信息。

这里单独说一下我司现在的做法,以更好的说明该格式的实际使用环境:

  1. 建立系统菜单。需要填写菜单的名称,和页面地址。多语言条件下填写多种名称。
  2. 创建权限组。需要填写权限组名称,并为该权限组分配页面级权限及数据级权限。
  3. 分配用户权限。将用户和某个权限组绑定。

以上配置均在前端有对应的页面进行操作。

编写导航钩子

上文中我们说过,需要在页面跳转时,将路由表和返回数据进行匹配,如果存在于返回数据中则正常跳转,如果不存在则跳转到任意页面。这里我们需要实现这个逻辑。以下为简单实现,只说明原理,不涉及细节。

// 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()
        }
    })
})

到这里我们已经完成了对页面访问的权限控制。接下来讲解数据级操作权限的实现。

数据级操作权限控制

数据操作权限的获取方式,我能够想到的是以下两种:

  • 用户登录后,获取权限列表时,直接在该数据中体现
  • 每次访问页面时请求后台,获取该页面的数据操作权限列表

不管项目中具体采用哪一种方法,或者使用两种方法的结合,其实现原理都是一样的:即将返回的数据操作权限列表增加到路由表中的对应页面的自定义字段中(如permission),然后在进入页面前,根据该自定义字段来判断页面中的元素是否需要显示。

这里的核心点是:如何将数据放置到路由表中,以及如何根据路由表中的字段在页面中进行判断。接下来则重点解决这两个问题。

我们先简单的修改一下登录后返回的权限列表数据,为其增加permission字段

var data = [
   ...
    {
        name: '系统组件',
        child: [
            {
                name: '介绍',
                path: '/components',
                // 为介绍页面增加查看按钮权限
                permission: ['view']
            },
            {
                name: '功能类',
                child: [
                    ...
                    {
                        path: '/components/pageTable',
                        name: '表格',
                        // 为表格页面增加导出、编辑权限
                        permission: ['outport', 'edit']
                    }
                ]
            }
        ]
    }
]    

将数据放置到路由表中

为了实现“将数据放置到路由表中”这个功能,我们只需要将目光放回到上文中说的导航钩子那里。在那边,我们有一个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

以上的方法便是对数据级权限(实质上是操作按钮)的显示控制。

路由控制完整流程图

至此为止,后台管理网站的权限控制流程就已经完全结束了,在最后我们总结一下完整的权限控制流程图。

Created with Raphaël 2.1.2 开始 进入登录页面 登录成功? 是否已经请求过权限菜单? 根据数据生成系统菜单 点击菜单,进入页面前 匹配权限菜单,是否有该页面访问权限? 获取该页面详细权限,变更页面权限元素 进入页面 定制化的无权访问页面 获取权限菜单列表 获取成功? yes no yes no yes no yes no

NEXT——登录及系统菜单加载


在下一章中将重点讲述使用常规数据结构生成需要的菜单,以及unique-opened模式下点击跟节点不能收回多级菜单的问题

源码


当前源码地址:https://github.com/harsima/vue-backend
请注意,该源码会不断更新(因为工作很忙不能保证定期更新)。源码涉及到的东西有超出本篇教程的部分,请酌情阅读。

本系列目录


Logo

Authing 是一款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务

更多推荐