「Spring Security」前后端分离菜单权限控制-前端动态路由

前端部分,这里基于vue-element-admin模板来演示,
vue-element-admin是一个后台前端解决方案,它基于vueelement-ui实现。

1. 安装 vue-element-admin

1
2
3
4
5
6
7
8
9
10
11
# 克隆项目
git clone https://github.com/PanJiaChen/vue-element-admin.git

# 进入项目目录
cd vue-element-admin

# 安装依赖, 建议不要用 cnpm 安装 会有各种诡异的bug 可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org

# 本地开发 启动项目
npm run dev

2. 改造前端路由挂载方式

vue-element-admin中权限的实现方式是:通过获取当前用户的权限去比对路由表,生成当前用户具有的权限可访问的路由表,通过router.addRoutes动态挂载到router上。

这里改造得更灵活一点,后台根据用户计算出可访问得菜单列表,直接返回用户可访问得菜单列表,前端也需要保存一份全的路由表,用户登录后得到可访问菜单,匹配前端保存的路由表然后动态挂载。

用户登录成功之后,在全局钩子router.beforeEach中拦截路由,判断是否已获得token,在获得token之后我们就要去获取用户的基本信息及可访问菜单,然后动态挂载路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* src/permission.js
*/
// router.beforeEach
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
// get user info
const { menus } = await store.dispatch('user/getInfo')
// generate accessible routes map based on menus
const accessRoutes = await store.dispatch('permission/generateRoutes', menus)
// dynamically add accessible routes
router.addRoutes(accessRoutes)
// ... other code
}

2.1 根据接口返回的菜单列表menus动态挂载路由

接口返回菜单数据:

动态挂载路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* src\store\modules\permission.js
*/
import { constantRoutes, asyncRoutes, afterRoutes } from '@/router'

/**
* 返回当前路由名称对应的菜单
* @param menus 菜单列表
* @param name 路由名称
*/
function filterMeun(menus, name) {
if (name) {
for (let i = 0; i < menus.length; i++) {
const menu = menus[i]
if (name === menu.name) {
return menu
}
}
}
return null
}

/**
* 通过后台请求的菜单列表递归过滤路由表
* @param routes asyncRoutes
* @param menus 接口返回的菜单
*/
export function filterAsyncRoutes(routes, menus) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
const meun = filterMeun(menus, tmp.name)
if (meun != null && meun.title) {
tmp.hidden = meun.hidden !== 0
// 显示的菜单替换后台设置的标题
if (!tmp.hidden) {
tmp.meta.title = meun.title
tmp.sort = meun.sort
}
if (meun.icon) {
tmp.meta.icon = meun.icon
}
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, menus)
}
res.push(tmp)
}
})
return res
}

/**
* 对菜单进行排序
*/
function sortRouters(accessedRouters) {
for (let i = 0; i < accessedRouters.length; i++) {
const router = accessedRouters[i]
if (router.children && router.children.length > 0) {
router.children.sort(compare('sort'))
}
}
accessedRouters.sort(compare('sort'))
}

/**
* 升序比较函数
*/
function compare(p) {
return (m, n) => {
const a = m[p]
const b = n[p]
return a - b
}
}

const state = {
routes: [],
addRoutes: []
}

const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}

const actions = {
generateRoutes({ commit }, menus) {
return new Promise(resolve => {
// 通过后台请求的菜单列表递归过滤路由表
const roleAsyncRoutes = filterAsyncRoutes(asyncRoutes, menus)
// 对可访问菜单进行排序
sortRouters(roleAsyncRoutes)
// 拼接尾部公共菜单
const accessedRoutes = roleAsyncRoutes.concat(afterRoutes)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}

export default {
namespaced: true,
state,
mutations,
actions
}

2.2 前端保存的全路径路由表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* src\router\index.js
*/
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)
import Layout from '@/layout'

/**
* 没有权限要求的基本路由
*/
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index')
}
]
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index'),
name: 'Dashboard',
meta: { title: '后台首页', icon: 'dashboard', affix: true }
}
]
},
{
path: '/profile',
component: Layout,
redirect: '/profile/index',
hidden: true,
children: [
{
path: 'index',
component: () => import('@/views/profile/index'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user', noCache: true }
}
]
}
]

/**
* 动态加载的路由
*/
export const asyncRoutes = [
{
path: '/sys',
component: Layout,
redirect: '/sys/menus',
alwaysShow: true, // will always show the root menu
name: 'SysSetting', // name必须和后台配置一致,不然匹配不到
meta: { title: '系统设置', icon: 'el-icon-s-tools' },
children: [
{
path: 'menus',
component: () => import('@/views/sys/menus/index'),
redirect: '/sys/menus/list',
name: 'SysMenus',
meta: { title: '菜单管理', icon: 'el-icon-menu' },
children: [
{
path: 'list',
hidden: true,
component: () => import('@/views/sys/menus/list.vue'),
name: 'SysMenuList',
meta: { title: '菜单列表' }
},
{
path: 'edit',
hidden: true,
component: () => import('@/views/sys/menus/form.vue'),
name: 'SysMenuEdit',
meta: { title: '编辑菜单' }
},
{
path: 'add',
hidden: true,
component: () => import('@/views/sys/menus/form.vue'),
name: 'SysMenuEdit',
meta: { title: '添加菜单' }
}
]
},
{
path: 'roles',
component: () => import('@/views/sys/roles/index'),
redirect: '/sys/roles/list',
name: 'SysRoles',
meta: { title: '角色管理', icon: 'lock' },
children: [
{
path: 'list',
hidden: true,
component: () => import('@/views/sys/roles/list.vue'),
name: 'SysRoleList',
meta: { title: '角色列表' }
},
{
path: 'edit',
hidden: true,
component: () => import('@/views/sys/roles/form.vue'),
name: 'SysRoleEdit',
meta: { title: '编辑角色' }
},
{
path: 'add',
hidden: true,
component: () => import('@/views/sys/roles/form.vue'),
name: 'SysRoleEdit',
meta: { title: '添加角色' }
}
]
},
{
path: 'users',
component: () => import('@/views/sys/users/index'),
redirect: '/sys/users/list',
name: 'SysUsers',
meta: { title: '用户管理', icon: 'user' },
children: [
{
path: 'list',
hidden: true,
component: () => import('@/views/sys/users/list.vue'),
name: 'SysUserList',
meta: { title: '用户列表' }
},
{
path: 'add',
hidden: true,
component: () => import('@/views/sys/users/form.vue'),
name: 'SysUserEdit',
meta: { title: '添加用户' }
},
{
path: 'edit',
hidden: true,
component: () => import('@/views/sys/users/form.vue'),
name: 'SysUserEdit',
meta: { title: '编辑用户' }
}
]
},
{
path: 'icons',
component: () => import('@/views/sys/icons/index'),
name: 'SysIcons',
meta: { title: '系统图标', icon: 'el-icon-picture', noCache: true }
}
]
},
/** when your routing map is too long, you can split it into small modules **/
// componentsRouter,
// chartsRouter,
// nestedRouter,
// tableRouter,
]

/**
* 没有权限要求的底部基本路由
*/
export const afterRoutes = [
{
path: 'external-link',
component: Layout,
children: [
{
path: 'https://www.test.com/',
meta: { title: '友情链接', icon: 'link' }
}
]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]

const createRouter = () =>
new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}

export default router

源码地址:https://github.com/chaooo/spring-security-jwt.git,
这里我将本文的前后端分离后台菜单权限控制放在github源码tag的V4.0中,防止后续修改后代码对不上。