权限控制模块
简介
动态路由可做模块层面的权限控制,但要想把权限做的更细,可以使用vue自定义指令,控制到具体的标签中,当然 使用递归解决权限管理表单也是本文的一大亮点
动态路由
vue路由配置 并且在跳转路由时获取后台存储的路由信息
动态路由配置代码,当然仅仅有这段代码还不能够支持使用,因为它还需要其他的代码如发送请求,向session中存储appToken等。但您可以在此获得动态路由相关的业务,其核心部分在于 fetchGetRouterMenu()请求获取路由动态部分,再通过二次转换得到elementUI导航栏菜单需要的导航栏数据 和 与静态路由部分合并成的整个路由。该部分需要的插件如下:
- 进度条插件,来自于 https://github.com/rstacruz/nprogress
npm install --save nprogress
- vue官方的路由,用于路由用户设定访问路径,将路径和组件映射起来
npm install vue-router --save
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from "@/store"
import fetchLogin, {getRouterMenu} from "@/api/users"; //本项目将所有的请求,统一到api目录,这里是引入自己的url请求方法
import {routes} from "./routes.js"; //前端存储的一些固定的路由 不需要动态路由存储的,比如“login”
import NProgress from "nprogress"; //进度条插件,用官网的话来讲: 涓涓细流动画告诉你的用户,一些事情正在发生!
import StringUtils from "@/js/StringUtils"; //自己的stringUtils
/**
* 通过vue.use 安装路由插件
*/
const router = new VueRouter({
mode: 'hash',
base: process.env.BASE_URL,
routes
})
Vue.use(VueRouter)
/**
* 重写路由的push方法,防止跳转相同路由时报错
*/
const routerPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return routerPush.call(this, location).catch(error => error)
};
NProgress.configure({ showSpinner: false }) //进度条配置关闭加载微调器
const whiteList = ["/login", "/auth-redirect"]; //没有appToken时允许跳转这些路由
/**
* 路由拦截器,所有路由跳转时都会进入该方法
*/
router.beforeEach(async (to, come, next) => {
NProgress.start(); //跳转路由时 开启进度条
if (Vue.prototype.$ViewConfig.appToken.get()) { //通过校验存储在session中的appToken, 来确定用户是否登录
if (to.path === "/login") { //单独校验当存在appToken,又需要跳转到login的情况(退出登录)
next({ path: "/" })
NProgress.done()
} else {
if (store.state.roles.length === 0) { //通过校验登陆时存的roles,进一步校验是否已经登录
try {
await GetUserInfo(); // 向后端认证是否登录
await getRouterMenu().then((res) => { //获取路由动态部分
let routesConcat = []
/* 核心部分,将通过请求获取到的路由动态部分加入到路由中*/
routesConcat = routes.concat(filterAsyncRoutes(res));
store.state.menuList = filterMenuList(routesConcat);
router.addRoutes(routesConcat)
next({ ...to, replace: true })
})
} catch (err) {
/* 认证登录报错,或者获取动态路由报错,重定向到登录页 */
Vue.prototype.$ViewConfig.outSystem()
Vue.prototype.$message.error(err || "Has Error")
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
next()
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach((to) => {
NProgress.done()
})
/**
* 二次校验登录信息
*/
const GetUserInfo = async () => {
if (Vue.prototype.$ViewConfig.appToken === "") {
throw Error("GetUserInfo: token is undefined!")
}
const { data } = await fetchLogin.fetchGetUserInfo()
if (!data) {
throw Error("认证失败,请先登录!!!")
}
// 存储一些与本文无关的认证属性(如appToken) 略....
}
/**
* 处理动态路由部分,主要是为了将表示路由地址的字符串转化为路由地址
*/
export const filterAsyncRoutes = (routers) => {
return routers.filter(router => {
if (router.component) {
if (router.component === "Layout") { // Layout组件特殊处理
router.component = (resolve) => require([`@/views/index.vue`],resolve)
} else {
const component = router.component
router.component = loadView(component, StringUtils.isNotEmpty(router.children))
}
}
if (router.children && router.children.length) {
router.children = filterAsyncRoutes(router.children)
}
return true
})
}
export const loadView = (view, state) => {
if (state) {
return (resolve) => require([`@/views/templateIndex.vue`], resolve) //路由途径文件,单独处理
} else {
return (resolve) => require([`@/views/${view}.vue`], resolve)
}
}
/**
* 轮询解析路由动态部分,生成elementUI导航菜单能使用的菜单数据,因为接收到的格式不能直接放到elementUI中使用,需要修改属性名
*/
export const filterMenuList = (routers) => {
let menuList = [];
routers.forEach(router => {
let menu = {};
if (router.menuName) {
if (!router.meta.hidden) { // 是否可见
menu.route = router.name;
menu.title = router.menuName;
menu.icon = "";
if (router.children && router.children.length) {
menu.children = filterMenuList(router.children)
}
menuList.push(menu)
}
}
})
return menuList
}
export default router
静态路由部分
在刚进入系统时需要一个路由从默认路径 / 重定向到 /login,而此时动态路由的请求还未发送,因此需要初始化写死一部分,这部分 本文中统一称为 静态路由部分
export const routes = [
{
path: '/',
component: () => import("@/views"),
meta: {
requireAuth: true
},
children: [
{
path: "/",
name: "home",
component: () => import("@/views/home"),
meta: {roles: []}
}
]
},
{
path: "/404",
name: "notFound",
component: () => import("@/views/404"),
meta: {roles: []},
},
{
path: "/login",
name: "login",
component: () => import("@/views/login"),
meta: {roles: []},
},
]
权限控制自定义指令
vue自定义指令用于权限控制算是一个很经典的用法了,网上一搜就可以搜到很多相关的介绍文章,但都是使用了 vue-directive: https://cn.vuejs.org/v2/api/#Vue-directive。若只有这段代码store.state.roles是不能被识别的,因为他是项目初始化时从后台获取到的一些,代表该角色拥有的权限的权限码 组成的数组
创建自定义指令
此自定义指令中主要业务是判断store.state.roles中是否有传入的权限码
/**
* 检验元素权限属性,若有权限保留,无权限则删除
*/
Vue.directive('permission', {
inserted: function (el, binding, vnode) {
const {value} = binding
let boolean = true;
const roles = store.state.roles; //获取session中存储的权限
if(!roles) {
return false;
}
if (value && value instanceof Array && value.length > 0) {
//该层if是为了排除多个权限控制一个按钮的情况,正常情况下不会进入该if
const permissionRoles = value
const hasPermission = permissionRoles.some(permissionRole =>{
return roles.includes(permissionRole+'')
})
if (!hasPermission) {
boolean = false
}
}else if(value) {
if (!roles.includes(value)) {
boolean = false
}
}
//没有权限则删除标签
if (!boolean){
el.parentNode && el.parentNode.removeChild(el)
}
},
})
使用自定义指令
使用自定义指令只需要在任意标签上加上v-permission的属性就可以校验登录的角色是否有允许查看该标签的权限了。其中 110 代表该新增按钮所需的权限码
<el-button type="primary" :sizse="$ViewConfig.size" @click="handleAdd()" v-permission="110">新增</el-button>
递归解决权限管理页面
了解了自定义指令如何在标签层面解决权限问题后,有个重要的问题,就是这些权限码如何而来呢?当然此处就不介绍上传权限码,存到数据库,读取这些crud操作了,主要讲讲如何上传这些权限码
权限管理页面的效果
先看看权限管理页面的效果
**
**
递归实现
由于此处子节点不确定数量,此处使用组件递归处理,当然由后端统计个数,然后用for循环解决也是可行的。
表单部分
表单部分为上图的表单,为了更能直观的看到递归实现的逻辑,此处代码只有上图中"权限"部分
- 该弹窗需要外部调用 this.$refs.configForm.addRole(); 开启弹窗
- 该段代码主要可分为三个部分:使用elementUI的多选框实现权限选中,获取权限列表及提交表单,子权限和父权限之间的联动
<style scoped>
.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{
border-color: #151516 !important;
}
</style>
<template>
<div>
<el-dialog title="角色" :visible.sync="isShow" width="800px" :close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true" top="5vh" @closed="resetForm">
<el-form ref="dataForm" :model="formData" :rules="rules" label-width="100px" size="mini" v-loading="loading">
<el-form-item label="权限:">
<template slot-scope="scope">
<el-checkbox-group v-model="checkList">
<template v-for="(root, rootIndex) in permissionList">
<div style="background-color: #63b8ce;padding: 5px 10px 0px;">
<el-checkbox :label="root.code" @change="changeRootCheck(root)"><span style="color: #FFF;">{{root.desc}}</span></el-checkbox>
</div>
<role-recursion :ref="'recursion_'+root.code"
:root="root"
:thisObj="thisObj"
:disabledArray=notSelectableList
@push-in-check-list-super="pushInCheckList(root.code)"
@remove-by-father-super="removeByCheckListSuper(root)"></role-recursion>
</template>
</el-checkbox-group>
</template>
</el-form-item>
<el-form-item>
<div style="float: right" v-if="flag !== 'detail'">
<el-button :size="$ViewConfig.size">取 消</el-button>
<el-button :size="$ViewConfig.size" type="primary" @click="submitForm">确 定</el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
// 引入递归部分
import role_recursion from "./role_recursion";
export default {
name: "role-list",
components:{
"role-recursion": role_recursion
},
data() {
return {
loading: true,
isShow: false,
formData: {
name: null,
description: null,
auths: null,
},
permissionList: [],// 可选权限
notSelectableList: [],// 不可选权限
checkList: [],// 选中权限
rules: {
name: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
],
},
thisObj: this
}
},
methods: {
/**
* 新增
*/
addRole() {
this.getPermissionList();
this.getModelList();
this.isShow = true;
this.formData = {
name: null,
description: null,
auths: null,
}
this.loading = false;
},
/**
* 获取权限列表
**/
getPermissionList(id) {
this.$Http.get("role/getShowRole",{id: id}).then(data => {
this.permissionList = data.permissionList;
this.notSelectableList = data.notSelectableList;
}).catch(err =>{
this.$message.error("获取权限列表失败")
})
},
/**
* 重置表单
**/
resetForm() {
this.modelOptions = [];
this.modelRole = null;
this.checkList = [];
this.$refs.dataForm.resetFields();
//permissionList为空数组时 不会渲染 role_recursion,此处防止新的notSelectableList未获取到就渲染了新的权限复选框
this.permissionList = [];
},
/**
* 提交表单
*/
submitForm() {
this.$refs.dataForm.validate((valid) => {
if(valid) {
this.formData.auths = this.checkList;
let url = "role/add";
this.loading = true;
this.$Http.post(url,this.formData,true).then(data => {
this.$parent.getTableList();
this.isShow = false;
}).finally(()=> {
this.loading = false;
});
}
})
},
/**
* 修改根权限事件
*/
changeRootCheck(menu) {
if (this.$StringUtils.isEmpty(menu.subList)) {
return
}
if (this.thisObj.checkList.includes(menu.code)) {
for (const check of menu.subList) {//循环添加子节点
this.pushInCheckList(check.code);
this.$refs['recursion_'+menu.code][0].changeChain(check);
}
} else {
for (const check of menu.subList) {//循环删除子节点
this.removeByCheckList(check.code);
this.changeRootCheck(check);
}
}
},
/**
* 移除顶级
*/
removeByCheckListSuper(menu) {
for (const check of menu.subList) {//校验子节点是否存在
if (this.thisObj.checkList.includes(check.code)) {
return
}
}
this.removeByCheckList(menu.code);
},
/**
* 如果不存在,则加入checkList
* 如果存在
*/
pushInCheckList(item) {
if(this.checkList.indexOf(item) === -1) {
this.checkList.push(item);
}
},
/**
* 移除checkList的元素
*/
removeByCheckList(item) {
let index = this.checkList.indexOf(item);
if(index !== -1) {
this.checkList.splice(index, 1);
}
},
}
}
</script>
递归部分
为了方便递归,将递归部分单独提出来,这段代码较另外几段代码理解起来有一定的难度,这里做一些解释吧
dom部分
- 这里一次递归表示上图中横着一行权限
- 它的v-for循环的是这样一段数据 1这里为了节约篇幅没有格式化,若需要格式化可以搜一下格式化json的在线网址
- dom部分的重点在于,在role-recursion中引用role-recursion 达到递归的效果,role-recursion是该递归组件的文件名
js部分
- props.thisObj:为了方便对父级的选中项做处理,这里传入父级的this
- props.disabledArray: role-recursion会将这个数组中的code禁用,当然,该层递归用过后会将该数组继承到下一次递归,以便下一次递归能正常禁用其中的code
- 因为是递归实现dom,子级和父级之间的多选框不能自动实现联动,需要手动实现联动效果
<template>
<div>
<div v-for="(menu, menuIndex) in root.subList" style="padding-left: 5px;display: inline">
<template v-if="menu.subList === null">
<el-checkbox
:label="menu.code"
@change="changeChain(menu)"
:disabled="disabled_[menu.code]">
<span style="display:-moz-inline-box; display:inline-block;">
{{ menu.desc }}
</span>
</el-checkbox>
</template>
<template v-else>
<el-checkbox
:label="menu.code" v-model="thisObj.checkList"
@change="changeChain(menu)"
:disabled="disabled_[menu.code]">
<span style="display:-moz-inline-box; display:inline-block;width: 100px;color: #428bca;">
{{ menu.desc }}
</span>
</el-checkbox>
<role-recursion :ref="'recursion_' + menu.code"
:root="menu"
:thisObj="thisObj"
:disabled="disabled_[menu.code]"
:disabledArray="disabledArray"
@push-in-check-list-super="pushInCheckListSuper(menu)"
@remove-by-father-super="removeByCheckListSuper(menu)"
class="recursion-portion" ></role-recursion>
</template>
</div>
</div>
</template>
<script>
export default {
//此处name为短横线name,dom递归时引用此name
name: "role-recursion",
props: {
//父级节点
root: {
type: Object,
required: true,
},
//调用方的this
thisObj: {
type: Object,
required: true,
},
//是否禁用该多选及其子多选
disabled: {
type: Boolean,
required: false,
default: false
},
//禁用这些code
disabledArray: {
type: Array,
required: false,
default: []
}
},
data() {
return {
disabled_: {},
}
},
created() {
for (let menu of this.root.subList) {
let includes = this.disabledArray.includes(menu.code);
this.disabled_[menu.code] = this.disabled || includes;
}
},
methods: {
//新增父级
pushInCheckListSuper(menu) {
this.thisObj.pushInCheckList(menu.code);
this.$emit('push-in-check-list-super');
},
//移除父级
removeByCheckListSuper(menu) {
for (const check of menu.subList) {//校验子节点是否存在
if (this.thisObj.checkList.includes(check.code)) {
return
}
}
this.thisObj.removeByCheckList(menu.code);
this.$emit('remove-by-father-super')
},
//勾选连锁
changeChain(menu) {
if (this.thisObj.checkList.includes(menu.code)){
if (this.disabled_[menu.code]) {
this.thisObj.removeByCheckList(menu.code);
return
}
if (menu.subList !== null) {
for (const check of menu.subList) {//新增子集
this.thisObj.pushInCheckList(check.code);
if (this.$refs['recursion_'+menu.code] !== undefined) {//迭代子集
this.$refs['recursion_'+menu.code][0].changeChain(check);
}
}
}
this.$emit('push-in-check-list-super')//新增父级
}else {
this.thisObj.removeByCheckList(menu.code);
if (menu.subList !== null) {
for (const check of menu.subList) {//移除子集
this.thisObj.removeByCheckList(check.code);
if (this.$refs['recursion_'+menu.code] !== undefined) {//迭代子集
this.$refs['recursion_'+menu.code][0].changeChain(check);
}
}
}
this.$emit('remove-by-father-super');//移除父级
}
},
}
}
</script>
<style scoped>
.recursion-portion{
margin-left: 9px;
padding-left: 50px;
/*box-shadow: inset 5px 0px 0 0 black;*/
}
</style>
[{“code”:100,“desc”:“我的应用”,“subList”:[{“code”:110,“desc”:“系统管理”,“subList”:[{“code”:111,“desc”:“查询”,“subList”:null},{“code”:112,“desc”:“新增”,“subList”:null},{“code”:113,“desc”:“更新”,“subList”:null},{“code”:114,“desc”:“删除”,“subList”:null},{“code”:115,“desc”:“创建”,“subList”:null}]},{“code”:120,“desc”:“权限管理”,“subList”:[{“code”:121,“desc”:“查询”,“subList”:null},{“code”:122,“desc”:“新增”,“subList”:null},{“code”:123,“desc”:“更新”,“subList”:null},{“code”:124,“desc”:“删除”,“subList”:null},{“code”:125,“desc”:“创建”,“subList”:null}]},{“code”:130,“desc”:“账号管理”,“subList”:[{“code”:131,“desc”:“查询”,“subList”:null},{“code”:132,“desc”:“新增”,“subList”:null},{“code”:133,“desc”:“更新”,“subList”:null},{“code”:134,“desc”:“删除”,“subList”:null}]},{“code”:140,“desc”:“SSO配置”,“subList”:[{“code”:141,“desc”:“查询”,“subList”:null},{“code”:142,“desc”:“更新”,“subList”:null}]},{“code”:150,“desc”:LDAP配置",“subList”:[{“code”:151,“desc”:“查询”,“subList”:null},{“code”:152,“desc”:“更新”,“subList”:null}]}]}] ↩︎