Spring Cloud Gateway +Oauth2 +JWT+Vue 实现前后端分离RBAC权限管理(前端实现)
上一篇文章讲解了后端的实现方式:后端实现如果你想了解后端的实现原理,可以先看后端的实现原理。本文的源码地址:源码
·
上一篇文章讲解了后端的实现方式:后端实现
如果你想了解后端的实现原理,可以先看后端的实现原理。本文的源码地址:
源码
其实所谓的权限控制,无非就是菜单权限控制和按钮权限控制。
菜单权限控制
首先说下布局:
<template>
<Layout style="height: 100%" class="main">
<Sider hide-trigger collapsible :width="256" :collapsed-width="64" v-model="collapsed" class="left-sider" :style="{overflow: 'hidden'}">
<side-menu accordion ref="sideMenu" :active-name="$route.name" :collapsed="collapsed" @on-select="turnToPage" :menu-list="menuList">
<!-- 需要放在菜单上面的内容,如Logo,写在side-menu标签内部,如下 -->
<div class="logo-con">
<img v-show="!collapsed" :src="maxLogo" key="max-logo" />
<img v-show="collapsed" :src="minLogo" key="min-logo" />
</div>
</side-menu>
</Sider>
<Layout>
<Header class="header-con">
<header-bar :collapsed="collapsed" @on-coll-change="handleCollapsedChange">
<user :message-unread-count="unreadCount" :user-avatar="userAvatar"/>
<language v-if="$config.useI18n" @on-lang-change="setLocal" style="margin-right: 10px;" :lang="local"/>
<error-store v-if="$config.plugin['error-store'] && $config.plugin['error-store'].showInHeader" :has-read="hasReadErrorPage" :count="errorCount"></error-store>
<fullscreen v-model="isFullscreen" style="margin-right: 10px;"/>
</header-bar>
</Header>
<Content class="main-content-con">
<Layout class="main-layout-con">
<div class="tag-nav-wrapper">
<tags-nav :value="$route" @input="handleClick" :list="tagNavList" @on-close="handleCloseTag"/>
</div>
<Content class="content-wrapper">
<keep-alive :include="cacheList">
<router-view/>
</keep-alive>
<ABackTop :height="100" :bottom="80" :right="50" container=".content-wrapper"></ABackTop>
</Content>
</Layout>
</Content>
</Layout>
</Layout>
</template>
<script>
import SideMenu from './components/side-menu'
import HeaderBar from './components/header-bar'
import TagsNav from './components/tags-nav'
import User from './components/user'
import ABackTop from './components/a-back-top'
import Fullscreen from './components/fullscreen'
import Language from './components/language'
import ErrorStore from './components/error-store'
import { mapMutations, mapActions, mapGetters } from 'vuex'
import { getNewTagList, routeEqual } from '@/libs/utils'
import routers from '@/router/routers'
import minLogo from '@/assets/images/logo-min.jpg'
import maxLogo from '@/assets/images/logo.jpg'
import './main.less'
export default {
name: 'Main',
components: {
SideMenu,
HeaderBar,
Language,
TagsNav,
Fullscreen,
ErrorStore,
User,
ABackTop
},
data () {
return {
collapsed: false,
minLogo,
maxLogo,
isFullscreen: false
}
},
computed: {
...mapGetters({
errorCount:'app/errorCount',
showmenuList: "app/menuList"
}),
tagNavList () {
return this.$store.state.app.tagNavList
},
tagRouter () {
return this.$store.state.app.tagRouter
},
userAvatar () {
return this.$store.state.user.avatarImgPath
},
cacheList () {
const list = ['ParentView', ...this.tagNavList.length ? this.tagNavList.filter(item => !(item.meta && item.meta.notCache)).map(item => item.name) : []]
return list
},
menuList () {
return this.showmenuList
},
local () {
return this.$store.state.app.local
},
hasReadErrorPage () {
return this.$store.state.app.hasReadErrorPage
},
unreadCount () {
return this.$store.state.user.unreadCount
}
},
methods: {
...mapMutations({
setBreadCrumb:'app/setBreadCrumb',
setTagNavList:'app/setTagNavList',
addTag:'app/addTag',
setLocal:'app/setLocal',
setHomeRoute:'app/setHomeRoute',
closeTag:'app/closeTag'
}),
...mapActions({
getUnreadMessageCount:'user/getUnreadMessageCount'
}),
turnToPage (route) {
let { name, params, query } = {}
if (typeof route === 'string') name = route
else {
name = route.name
params = route.params
query = route.query
}
if (name.indexOf('isTurnByHref_') > -1) {
window.open(name.split('_')[1])
return
}
this.$router.push({
name,
params,
query
})
},
handleCollapsedChange (state) {
this.collapsed = state
},
handleCloseTag (res, type, route) {
if (type !== 'others') {
if (type === 'all') {
this.turnToPage(this.$config.homeName)
} else {
if (routeEqual(this.$route, route)) {
this.closeTag(route)
}
}
}
this.setTagNavList(res)
},
handleClick (item) {
this.turnToPage(item)
}
},
watch: {
'$route' (newRoute) {
const { name, query, params, meta } = newRoute
this.addTag({
route: { name, query, params, meta },
type: 'push'
})
this.setBreadCrumb(newRoute)
this.setTagNavList(getNewTagList(this.tagNavList, newRoute))
this.$refs.sideMenu.updateOpenName(newRoute.name)
}
},
mounted () {
/**
* @description 初始化设置面包屑导航和标签导航
*/
this.setTagNavList()
this.setHomeRoute(routers)
const { name, params, query, meta } = this.$route
this.addTag({
route: { name, params, query, meta }
})
this.setBreadCrumb(this.$route)
// 设置初始语言
this.setLocal(this.$i18n.locale)
// 如果当前打开页面不在标签栏中,跳到homeName页
if (!this.tagNavList.find(item => item.name === this.$route.name)) {
this.$router.push({
name: this.$config.homeName
})
}
// 获取未读消息条数
this.getUnreadMessageCount()
}
}
</script>
我们可以看到布局中存在的menuList,这个就是需要渲染出来的菜单列表。所以只要控制menuList就行。
那我们来看下menuList的实现。
getters: {
menuList: (state, getters, rootState) => getMenuByRouter(routers, rootState.user.access),
errorCount: state => state.errorList.length
},
getMenuByRouter的实现如下
/**
* @param {Array} list 通过路由列表得到菜单列表
* @returns {Array}
*/
export const getMenuByRouter = (list, access) => {
console.log("-------------"+list)
let res = []
forEach(list, item => {
if (!item.meta || (item.meta && !item.meta.hideInMenu)) {
let obj = {
icon: (item.meta && item.meta.icon) || '',
name: item.name,
meta: item.meta
}
if ((hasChild(item) || (item.meta && item.meta.showAlways)) && showThisMenuEle(item, access)) {
obj.children = getMenuByRouter(item.children, access)
}
if (item.meta && item.meta.href) obj.href = item.meta.href
if (showThisMenuEle(item, access)) res.push(obj)
}
})
return res
}
export const hasChild = (item) => {
return item.children && item.children.length !== 0
}
const showThisMenuEle = (item, access) => {
if (item.meta && item.meta.access && item.meta.access.length) {
if (hasOneOf(item.meta.access, access)) return true
else return false
} else return true
}
foreach的工具方法如下:
export const forEach = (arr, fn) => {
if (!arr.length || !fn) return
let i = -1
let len = arr.length
while (++i < len) {
let item = arr[i]
fn(item, i, arr)
}
}
我们通过工具方法可以看到过滤条件是有router中的参数确定的。那么我们接下来看下router的配置
import Main from '@/components/main'
/**
* iview-admin中meta除了原生参数外可配置的参数:
* meta: {
* title: { String|Number|Function }
* 显示在侧边栏、面包屑和标签栏的文字
* 使用'{{ 多语言字段 }}'形式结合多语言使用,例子看多语言的路由配置;
* 可以传入一个回调函数,参数是当前路由对象,例子看动态路由和带参路由
* hideInBread: (false) 设为true后此级路由将不会出现在面包屑中,示例看QQ群路由配置
* hideInMenu: (false) 设为true后在左侧菜单不会显示该页面选项
* notCache: (false) 设为true后页面在切换标签后不会缓存,如果需要缓存,无需设置这个字段,而且需要设置页面组件name属性和路由配置的name一致
* access: (null) 可访问该页面的权限数组,当前路由设置的权限会影响子路由
* icon: (-) 该页面在左侧菜单、面包屑和标签导航处显示的图标,如果是自定义图标,需要在图标名称前加下划线'_'
* beforeCloseName: (-) 设置该字段,则在关闭当前tab页时会去'@/router/before-close.js'里寻找该字段名对应的方法,作为关闭前的钩子函数
* }
*/
export default [
{
path: '/login',
name: 'login',
meta: {
title: '登录',
hideInMenu: true
},
component: () => import('@/view/login/login.vue')
},
{
path: '/',
name: '_home',
redirect: '/home',
component: Main,
meta: {
hideInMenu: false,
notCache: true
},
children: [
{
path: '/home',
name: 'home',
meta: {
hideInMenu: false,
title: '首页',
notCache: true,
icon: 'md-home'
},
component: () => import('@/view/index/index.vue')
}
]
},
{
path: '/system',
name: 'system',
meta: {
icon: 'logo-buffer',
title: '系统设置'
},
component: Main,
children: [
{
path: 'user',
name: 'user',
meta: {
access:['CENTEER_USER'],
icon: 'md-arrow-dropdown-circle',
title: '用户管理'
},
component: () => import('@/view/sys-set/user/index.vue')
},
{
path: 'role',
name: 'role',
meta: {
access:['CENTEER_USER'],
icon: 'md-trending-up',
title: '角色管理'
},
component: () => import('@/view/index/index.vue')
},
{
path: 'permission',
name: 'permission',
meta: {
access:['CENTEER_PERMISSION'],
icon: 'ios-infinite',
title: '权限管理'
},
component: () => import('@/view/index/index.vue')
}
]
}
]
到这里,菜单权限配置完成。下面我们来说下按钮配置权限
按钮权限控制
<template>
<div>
<Button style="margin: 10px 0;" type="primary" v-if="addUserAccess">添加</Button>
<Table border :columns="columns12" >
<template slot-scope="{ row }" slot="name">
<strong>{{ row.name }}</strong>
</template>
<template slot-scope="{ row, index }" slot="action">
<Button type="primary" size="small" style="margin-right: 5px" @click="show(index)" v-if="viewUserAccess">View</Button>
<Button type="error" size="small" @click="remove(index)" v-if="deleteUserAccess">Delete</Button>
</template>
</Table>
</div>
</template>
我们可以看到对于按钮我们都增加if权限配置
computed: {
access () {
return this.$store.state.user.access
},
addUserAccess () {
return hasOneOf(['CENTER_USER_ADD'], this.access)
},
viewUserAccess(){
return hasOneOf(['CENTER_USER_VIEW'], this.access)
},
deleteUserAccess(){
return hasOneOf(['CENTER_USER_DELETE'], this.access)
}
}
核心的工具方法hasOneOf
/**
* @param {Array} target 目标数组
* @param {Array} arr 需要查询的数组
* @description 判断要查询的数组是否至少有一个元素包含在目标数组中
*/
export const hasOneOf = (targetarr, arr) => {
return targetarr.some(_ => arr.indexOf(_) > -1)
}
简单的就完成了按钮的权限控制了。那么对应的就需要一个核心方法来获取权限也就是获取用户的接口。
权限判断
那么如果没权限的判断,就得在路由处进行处理
import Vue from 'vue'
import Router from 'vue-router'
import routes from './routers'
import iView from 'view-design'
import store from '@/store'
import {TOKEN, getToken,removeStore} from '@/libs/auth';
import {setTitle,canTurnTo} from '@/libs/utils'
import config from '@/config'
Vue.use(Router)
const router = new Router({
routes,
mode: 'history'
})
const LOGIN_PAGE_NAME = 'login'
const turnTo = (to, access, next) => {
if (canTurnTo(to.name, access, routes)) next() // 有权限,可访问
else next({ replace: true, name: 'error_401' }) // 无权限,重定向到401页面
}
router.beforeEach((to, from, next) => {
iView.LoadingBar.start()
const token = getToken(TOKEN)
if (!token && to.name !== LOGIN_PAGE_NAME) {
// 未登录且要跳转的页面不是登录页
next({
name: LOGIN_PAGE_NAME // 跳转到登录页
})
} else if (!token && to.name === LOGIN_PAGE_NAME) {
// 未登陆且要跳转的页面是登录页
next() // 跳转
} else if (token && to.name === LOGIN_PAGE_NAME) {
// 已登录且要跳转的页面是登录页
next({
name: "home" // 跳转到homeName页
})
}
else {
if (store.state.user.hasGetInfo) {
turnTo(to, store.state.user.access, next)
} else {
store.dispatch('user/getUserInfo').then(user => {
// 拉取用户信息,通过用户权限和跳转的页面的name来判断是否有权限访问;access必须是一个数组,如:['super_admin'] ['super_admin', 'admin']
turnTo(to, user.data.access, next)
}).catch(error => {
removeStore()
next({
name: LOGIN_PAGE_NAME
})
})
}
}
})
router.afterEach(to => {
setTitle(to, router.app,config.title)
iView.LoadingBar.finish()
window.scrollTo(0, 0)
})
export default router
获取用户信息的方法可以查看后台接口的具体实现
更多推荐
已为社区贡献2条内容
所有评论(0)