Vue单页面应用如何进行权限管理
Vue 之类的前端框架的出现让 SPA 更加方便,同时也带来了一些小问题,例如一个拥有用户权限的应用,如何根据用户的不同权限来展示页面和功能?文本主要整理讨论与权限管理有关的问题及其解决方案。
简述
在当前的工业互联网项目中,智能表计的数据获取到以后需要整理分析,然后为不同角色的用户来指定不同的浏览规则,于是就需要前进的组件和路由进行权限管理。
首先,我们应该先弄懂两层含义,第一是对组件的权限管理,另一种是对路由页面的权限管理。页面路由的权限管理比较好理解,类似于早期的管理员页面,不同角色有自己可以浏览的页面,这种情况下,页面是一个完整的功能单位,此时权限可以基于页面来划分。而组件权限就有所不同了,这类需求虽不多,但在开发中还是有碰到,往往是一个页面中,不同的用户可以操作的功能有所不同。
组件权限举个简单的例子,有一个设备列表,管理员可以对这个列表进行增删,但是另一个用户组的权限则只能浏览,这些都可以在一个页面中,但是功能却有所不同。
获取权限
获取权限可以通过用户登录来完成,可以分如下几步:
- 用户通过前端页面完成登录,接口返回用户的 id 和所在组;
- 应用根据用户的 id 和用户组,获取用户权限和组权限;
- 应用将用户权限和组权限合并(用户权限优先于组权限),生成路由权限表和组件权限表;
- 前端根据路由权限表加载用户的默认页面。
从上面的过程可以知道几点:
- 用户注册登录页面可以与应用分开,如果站点应用较多,也可以使用全局的 passport 组件来登录;
- 由于预先不知道用户角色,所以前端应用可以按照最小体积来打包,待获取到用户信息后再异步加载;
- 组件同上。
根据上面的方案,又要有两种情况,一种是使用第三方UI组件,另一种是使用自己开发组件,那么这种了情况下组件的权限又该如何获得呢?
下面我将一一解决上面的问题。
路由权限管理
路由权限是比较好理解的,也是比较好实现的。
例如,通过用户登录,我们可以得知当前用户的 id 和用户组,所以合并这两个权限以后,就会得到一个新的路由组,这个组里的路由按理都是可以访问的,不再这个组里的路由就是不能访问的,不能访问的路由便需要拦截。
在 Vue 中,对路由的拦截可以在 router.beforEach 中进行。
例如,如果应用必须要求用户先登录,那么可以通过检查 store 中的 state 数据来判断,未登录时则通过路由守卫跳转至登录页面。
假设通过 Vuex 创建了一个状态管理的 store,store.state 有以下定义:
const store = new Vuex.Store({
state: {
userinfo: {
name: "Tom",
nickname: 'Tom',
groupid: 2,
sessionid: '1c24g5b67j7l8'
},
routes: [
'/',
'/home',
'/about'
]
},
mutations: {
},
getters: {
userinfo(state) {
return state.userinfo;
},
routes(state) {
return state.routes;
}
}
})
上面代码的 routes 是可路由的列表,这个列表又用户登录后获取的权限合并而来。
那么,在路由守卫中可以这样做权限判断:
router.beforeEach((to, from, next) => {
// 未登录,跳转到登录页
if (to.fullPath !== '/login' && !store.getters.userinfo) {
next({path: '/login'})
}
// 已登录,则判断用户是否有权访问
else if (store.getters.routes.indexOf(to.fullPath) > -1) {
next()
}
});
不在 routes 中的路由将不允许跳转。
组件权限
与路由权限不同,组件权限会稍微复杂一点,这是由于路由权限可以以页面为最小单位,但是组件却是页面的组成部分,粒度更小。
上文中有说,组件的权限可以分使用第三方的和不使用第三方的,使用第三的由于组将已经被封装好,我们是不回去改变组件源码的,所以只能对其进行扩展。而自行开发的则相对灵活一些,可以由开发者自行处理权限。
为了统一,我们这里可以统一一下,全部归为私有组件,以便增加灵活性,对于使用第三方组件的情况,我们可以通过组件包裹来变成私有组件。
例如,有 element-ui
组件如下:
<el-button type="primary">用户按钮</el-button>
通过包裹可得:
<template>
<div class="sl-buttom">
<el-button type="primary">用户按钮</el-button>
</div>
</template>
然后定义组件功能:
import defaultPermisson from "@/permisson/sl-button"
export default {
name: 'sl-buttom',
data: () => {
return {
permission: {}
}
},
created() {
let userinfo = this.$store.getters.userinfo;
let groupid = userinfo.groupid;
let uid = userinfo.uid;
// 设置默认组权限
this.permission = defaultPermisson[groupid];
},
methods: {
postData () {
console.log("hello world post data");
}
}
}
文件夹 permisson
定义了组件的默认权限,以这个 button 为例,可以设置按钮的文字,按钮颜色,已经按钮是否可点击等。然后将权限数据赋值到组件上。
<template>
<div class="sl-buttom">
<el-button
v-show="permission.show"
:type="permission.type || 'primary'"
:disabled="permission.disabled || false"
>{{permission.name || '用户按钮'}}</el-button>
</div>
</template>
然后我们只需要在 sl-button
权限文件中返回权限定义即可,这个权限完全可以独立定义,也可以又后台来生成,这样就可以把权限和组件分开来管理。权限文件可以这样定义:
// /permisson/sl-button.js
export default {
default: {
name: "默认权限",
show: true,
disabled: false,
type: 'primary'
},
group1: {
name: "组1权限",
show: true,
disabled: true,
type: 'default'
},
group1: {
name: "组1权限",
show: false,
disabled: true,
type: 'default'
}
}
以上只是一个示例。
使用场景
那么,哪些情况下需要使用权限管理呢?路由权限很好理解,不需要用户访问的页面我们不允许访问即可,那么组件权限如何在页面中体现呢?
上面我介绍的组件权限是一种实现方式,并不代码需要将权限定义的粒度划分到一个按钮上。在实际的开发中,我们往往是对具有一定功能的组件集合,或者一个大型组件才会使用权限定义,另外一个场景就是给用户显示菜单,显示功能区,那么这种情况上述的组件定义就很方便了。
一般来说,我们对一个组件可以设置的维度有:
- 是否显示(包括显示和不显示,以及如果显示的话样式是否有定义);
- 是否可读(是否显示一些特定数据);
- 是否可操作(例如用户的点击操作等);
例如有一些页面我们是不希望用户访问的,那么在导航里就可以对响应的组件设置为不显示;另一些组件我们希望用户可以看到,但是仅只读,不能操作,那么就可以通过权限将其设置为想要的状态。
用户权限的同步
读者可能注意到,上面组件的权限使用的时候默认定义,那么如果有一些功能权限是这个用户所独有的,按照用户权限优先与组权限的原则,我们应该去同步获取用户权限的定义,这个时候就需要与接口服务器通信,来同步获取用户权限,我们可以通过一个 Promise 来实现。
import defaultPermisson from "@/permisson/sl-button"
export default {
name: 'sl-buttom',
data: () => {
return {
permission: {}
}
},
created() {
let userPermis = this.store.getters.userPermis;
let userinfo = this.store.getters.userinfo;
let groupid = userinfo.groupid;
let uid = userinfo.uid;
// 如果有用户权限则使用用户权限,没有用户权限则使用默认的组权限
this.permission = userPermis.indexOf(this.name) ? userPermis[this.name] : defaultPermisson[groupid];
// 这里异步获取用户权限,在获取成功后更新当前的权限
this.permissionGette(uid).then(userPermissions => {
// 通过 Vuex 提供的 store 将用户权限缓存起来
this.store.commit('updateUserPermisson', userPermissions);
// 更新当前的权限数据
this.permission = userPermissions;
})
},
methods: {
postData () {
console.log("hello world post data");
}
}
}
引申:如何控制权限粒度
细心的朋友可能会发现,上面组件权限的控制中,可能存在一个空窗期,即我们先给组件赋值了组权限,然后才去拉取用户权限,在用户权限返回之前,这个组件可能拥有的是组权限,如何组权限与用户权限不一样,就会造成不一致的情况。
确实,这是一个问题,所以才要谈控制权限粒度。
控制权限的粒度主要设备组件权限的控制粒度,包括:
- 什么样的组件需要定义权限,什么样的组件不需要定义权限;
- 什么时候拉取用户权限。
上面两个问题搞清楚后其实组件权限的问题也就比较好处理了。
首先,前面已经有介绍,不是所有的组件都需要进行权限定义,而是需要根据业务来定义。在业务上,一般权限是用来区分功能的,或者说是能力的。例如有一个权限组对某个数据可读可写,但另一个用户组却只能读不能写,那么读写就是功能的区分了。
如果这个读写操作时通过一个按钮组件来实现的,那么就是说这个组件具备拥有权限管理的必要,这个时候我们才会为这个组件定义权限。
第二个问题,何时拉取用户权限?
上面我们讨论了什么是用户权限什么是组权限,用户权限优先于组权限,如果一个大型 SPA 项目所涉及的用户权限比较多又比较复杂,我们可以在进入到当前路由页面的时候再拉取,这样的粒度会更小。如果用户权限不多,那么可以在用户登录的时候直接拉取。
总结
虽然前段可以实现视觉上的权限管理,但是最后的关卡还是在后端,尤其功能上的权限。功能权限一把会与组件绑定,所以后端需要设置后对功能接口的拦截,防止用户恶意操作。