文章目录
前言
一个系统的权限控制,核心在于后端的处理,前端负责让用户感知到有权限这样一个东西存在。
对于前端在一套权限控制方案中能够做什么,我罗列了一下几个方面
- 按钮的控制
- 菜单的显示
- 路由的权限
- 接口的校验
以下案例用vue来做举例
按钮的控制
主要的体现是一个按钮我们通过是否渲染的方式或者是否能够点击的方式,向用户告知这个按钮是有选项控制的。
在这里我推荐前端去说服产品采用是否渲染的方式,因为按钮的禁止点击可以通过控制台去做修改,如果用户通过手段放开点击的话,我们还需要对点击事件做特殊处理(例如点击了还要判断是否有权限,没有就不执行核心代码),这样子每个和权限相关的按钮都要特殊处理,太麻烦了。
是否渲染的做法
自定义指令(推荐)
具体的方案,咱们可以采用自定义指令的方式,我这里有记录怎么写
函数式组件
用函数式组件,来判断是否渲染,这是个.vue文件
<script>
import { check } from "../utils/auth";
export default {
functional: true,
props: {
authority: {
type: Array,
required: true // 必须传递的属性
}
},
render(h, context) {
const { props, scopedSlots } = context;
return check(props.authority) ? scopedSlots.default() : null; // 权限通过就返回插槽内容
}
};
</script>
然后,再别的组件中,把需要判断显示的子组件当作插槽给函数式组件判断
<Authorized :authority="['admin']">
<SettingDrawer />
</Authorized>
插槽组件(推荐)
这种方式虽然每次都要把按钮写到组件标签里,但是好处就是很灵活:
<template>
<slot v-if="showSlot" :userPermisstions="permissions"></slot>
</template>
<script setup>
import { computed } from 'vue';
import { useAuth } from './useAuth'; // 当前用户的页面上的权限
const props = defineProps({
permission: {
type: [String, Array],
}
})
const { permissions } = useAuth()
const showSlot = computed(() => {
if (!props.permission) {
// 无权限传入,直接显示
return true
}
if (!permissions) {
return false
}
if (Array.isArray(props.permission)) {
return props.permission.every(p => permissions.value.includes(p))
} else {
return permissions.value.includes(props.permission)
}
})
</script>
日常用法和函数式组件方案一样,但是如果对于一些自定义场景就函数式组件就支持不了了。例如某个没有权限的按钮只是禁用而已,用我上面写的组件就可以灵活实现:
<Authorized :authority="['admin']">
<template #default="{pagePermissions}">
<SettingDrawer :disabled="!pagePermissions.includes('setting-drawer-btn')"/>
</template>
</Authorized>
数据怎么设计
这里要给个建议,之前看到有公司的按钮权限数据设计是一股脑把当前用户拥有的按钮权限全部返回给前端,例如:
btnAuthList = [
'user-center-edit',
'user-center-del',
'pay-own-add',
// ...
]
这种方式颗粒度太粗了,例如有个页面组件是共用的,a页面和b页面都用到,但是这个组件上的按钮t是在a页面没有权限,在b页面有权限,这种情况下这样的设计是满足不了场景的。
我的建议是按钮的权限数据随着路由一起给,例如a页面的按钮数据为一组,b页面的按钮数据为一组,例如:
btnAuthList = {
'user/center' : ['user-center-edit', 'user-center-del'],
'pay/own' : ['pay-own-add'],
// ...
}
然后在自定义指令中针对这样的方案做下处理即可。
其他方案
这个是我在https://fe.ecool.fun/ 前端面试宝典看来的,引用下。其实和上面说的差不多,只是把按钮权限数据一起放进了路由表里。
首先配置路由
{
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>
菜单的显示
菜单的显示和路由配置我认为是需要解耦的,因为之前所在公司中有项目的菜单数据是依赖了路由数据,会带来一个问题就是路由数据必定是大于菜单数据的,因为不是所有的页面都需要通过菜单导航点击进入。
这里就不讨论耦合的处理方式了。
最好,菜单的显示独立一套数据,路由的配置也独立一套数据,都由后端返回给前端。这样前端解耦处理,比较灵活。
菜单的数据设计就比较简单了,尽量贴合第三方组件库的Menu组件数据要求即可。
路由的配置
路由的配置要做到安全一定要后端根据用户的权限状态筛选完后返回,这样才是最安全的。然后前端采用动态路由的方式,将返回的路由插入路由表中。
这里的路由表其实包含两类路由,一类是不需要权限的路由配置,可称为公有路由,也就是每个用户共同拥有的路由,这个就可以不需要后端返回。一个就是后端筛选完后的路由,可称为私有路由。
返回的数据格式简化设计如下
[{
path: '/user', // 路由路径
component: 'User', // 对应的前端组件文件名称
name: '用户设置', // 对应的前端页面名称
meta: {
btnPermissions: ['admin', 'supper', 'normal'] // 如果按钮权限不解耦的话,也可以在路由数据中返回
},
menu: 'user', // 对应亮起的菜单按钮
children: [ // 子路由
{
path: 'center',
component: 'UserCenter',
name: '用户中心',
menu: 'user', // 对应亮起的菜单按钮
},
{
path: 'message',
component: 'Message',
name: '信息通知',
menu: 'user', // 对应亮起的菜单按钮
}
]
}
// ...
]
当然不能保证所有场景都适用,像上面的例子中,前端需要把通过组件名称把组件引入对应上,做成路由表插入。然后菜单亮起的逻辑写一写,就差不多了。
具体菜单和路由配置怎么落地实现,我以后专门写个文章。
接口的校验
当用户登录后,后端返回token前端拿到后进行存储,在axios的请求拦截器中,把token带上即可。后端就会根据token识别用户,判断当前用户是否有权限调用接口功能。
没有权限返回403,前端在axios的响应拦截器中做无权限提示。返回401的话,也做登录过期的提示处理。
补充
权限更新
如果在后台修改了某个用户的权限设置,在客户端怎么做到及时更新权限呢。
我之前公司采用的是退出登录,在重新登陆获取一遍权限数据的方案。我觉得不太好,有两个弊端。
一、权限更新不及时,虽然按钮相关调用的接口已经更新了校验,但是页面上呈现的还是之前权限的样子,有些页面还能够正常访问(除非后端能做到所有接口都带校验,这样就算访问了页面也不会有数据)。
二、这种重新登录获取权限数据的方式,说明是把权限数据放进浏览器缓存中了。这样数据就暴露了出来,可以通过修改的方式影响页面的显示。
所以,我觉得最好是前端只要页面主动刷新了,或重新登陆,都重新获取一遍权限数据,并且将数据缓存在状态管理中(vuex)。当然,要是前后端能建立websock通信最好了,后台一改权限立马通知客户端浏览器刷新。
同一个组件在一个页面复用,但按钮权限不一样怎么办
具体就是,页面a,同时复用了组件b,但是第一个组件b1能有的按钮权限为c、d按钮,第二个组件b2能有的按钮权限为c按钮,这可咋办?
暂时没想到好的解法,日后补充。
尾巴
用了上面的方案,在后台中,每次开发一个页面最多要配置三个地方:按钮、路由、菜单,一般也就是一个路由。
以后有空了再来补充一下具体代码实现。