Skip to content
This repository was archived by the owner on May 12, 2020. It is now read-only.

Commit fedc43d

Browse files
committed
feat(permission): adjust routes filter strategy
filter rotues according to user api access rather then simple user roles
1 parent 4fa2cae commit fedc43d

10 files changed

Lines changed: 313 additions & 83 deletions

File tree

src/pages/Private/index.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<template>
22
<section class="access__static">
3-
Current user role is <strong>{{$store.getters['login/role']}}</strong>
3+
Current route is
4+
<strong>{{$route.path}}</strong>
45
</section>
56
</template>
67

78
<script>
8-
export default {
9-
}
9+
export default {}
1010
</script>
1111

1212
<style lang='scss' scoped>
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import router from 'ROUTER'
2+
import store from 'STORE'
3+
import loginTypes from 'STORE/modules/login/mutations/types'
4+
import NProgress from 'nprogress'
5+
import 'nprogress/nprogress.css'
6+
import { tokenFromStorage, userInfoFromStorage } from 'UTILS/storage'
7+
import { MessageBox } from 'element-ui'
8+
import createDynamicRoutes from '../create-routes'
9+
import constantRoutes from 'ROUTER/routes/constant'
10+
11+
NProgress.configure({ showSpinner: false })
12+
13+
const WHITELIST = ['/login']
14+
15+
// Used to determine whether private routes has been added
16+
let HAS_ROUTES_ADDED = false
17+
18+
function setDynamicRoutesToStorage (roles) {
19+
const currentRole = Array.isArray(roles) ? roles[0] : roles
20+
store.commit(
21+
`login/${loginTypes.SET_DYNAMIC_ROUTES}`,
22+
createDynamicRoutes(currentRole)
23+
)
24+
}
25+
26+
function setGlobalRoutesToStorage () {
27+
store.commit(`login/${loginTypes.SET_ALL_ROUTES}`, [
28+
...constantRoutes,
29+
...store.getters['login/dynamicRoutes']
30+
])
31+
}
32+
33+
function errorHandler (e, next, redirectPath) {
34+
MessageBox({
35+
title: 'Error',
36+
message: 'We got a error when fetching user access.',
37+
type: 'error',
38+
showClose: false
39+
})
40+
.then(() => store.dispatch('login/userLogout'))
41+
.then(() =>
42+
next({
43+
path: `/login?redirect=${redirectPath}`,
44+
replace: true
45+
})
46+
)
47+
NProgress.done()
48+
console.error(e)
49+
}
50+
51+
function addRoutesToRouter () {
52+
router.addRoutes(store.getters['login/dynamicRoutes'])
53+
console.log(
54+
'%c[Routes creation]: routes has been added!',
55+
'color: yellow',
56+
store.getters['login/dynamicRoutes']
57+
)
58+
}
59+
60+
function createRoutesMap (to, next) {
61+
setDynamicRoutesToStorage(store.getters['login/role'])
62+
setGlobalRoutesToStorage()
63+
addRoutesToRouter()
64+
HAS_ROUTES_ADDED = true
65+
// 1. MUST invoke `next({ ...to, replace: true })` to prevent route matching
66+
// from occurring before route is added
67+
// 2. use `to` route to replace routes occurring before private routes is added
68+
return next({ ...to, replace: true })
69+
}
70+
71+
/**
72+
* @description Login state validation
73+
*/
74+
router.beforeEach((to, from, next) => {
75+
NProgress.start()
76+
77+
// Jump to target path directly If target path has been included by white list
78+
if (WHITELIST.includes(to.path)) {
79+
return next()
80+
}
81+
82+
// ! State: User has been logged in (local token).
83+
if (tokenFromStorage.getItem()) {
84+
// 1. fetch RolesMap if necessary (when RolesMap stored by back-end)
85+
86+
// local storage has a accessToken record, but current `login/accessToken`
87+
// vuex state is empty string when user activate a new session (eg. new
88+
// browser tab)
89+
if (!store.getters['login/accessToken']) {
90+
return store
91+
.dispatch('login/fetchUserAccess', tokenFromStorage.getItem())
92+
.then(() => {
93+
// fill vuex state with user information to prevent infinity loop.
94+
store.commit(
95+
`login/${loginTypes.SET_ACCESS_TOKEN}`,
96+
tokenFromStorage.getItem()
97+
)
98+
store.commit(
99+
`login/${loginTypes.SET_USER_INFO}`,
100+
JSON.parse(userInfoFromStorage.getItem())
101+
)
102+
})
103+
.then(() => createRoutesMap(to, next))
104+
.catch(e => errorHandler(e, next, to.path))
105+
}
106+
107+
// 2. confirm route access by user role, including global routes creation.
108+
if (!store.getters['login/role']) {
109+
// 2.1 fetch user role, then routes creation based on user role.
110+
return store
111+
.dispatch('login/fetchUserAccess', tokenFromStorage.getItem())
112+
.then(() => createRoutesMap(to, next))
113+
.catch(e => errorHandler(e, next, to.path))
114+
}
115+
116+
// (2.2 optional) Regenerate private routes based on user role when page
117+
// reload, because vuex state will be preserved by vuex-persistedstate when
118+
// page reload.
119+
if (store.getters['login/role'] && !HAS_ROUTES_ADDED) {
120+
console.log(
121+
'%c[Routes creation]: Activate private routes regeneration process !',
122+
'color: yellow;'
123+
)
124+
return createRoutesMap(to, next)
125+
}
126+
127+
// 2.2 filter route
128+
if (
129+
!to.meta.roles ||
130+
to.meta.roles.includes(store.getters['login/role'][0])
131+
) {
132+
next()
133+
} else {
134+
next({
135+
path: `/403?redirect=${to.path}`,
136+
replace: true
137+
})
138+
}
139+
} else {
140+
// ! State: user logout
141+
next({
142+
path: `/login?redirect=${to.path}`,
143+
replace: true
144+
})
145+
}
146+
})
147+
148+
router.afterEach((to, from) => {
149+
NProgress.done()
150+
})
151+
152+
export default router

src/permission/filter-routes.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import routes from 'ROUTER/routes/dynamic'
2+
3+
/**
4+
* @description 根据后端回传的当前用户权限表创建一个用于过滤 routes 的 hashmap
5+
* @param {Array} accesses 后端回传的当前用户权限表
6+
*/
7+
export function createAccessMap (accesses) {
8+
return accesses.reduce((accessMap, access) => {
9+
const accessNameList = access.access.split('.')
10+
// accessNameList[1] 表示某个模块的名称,如 device
11+
// accessNameList[2] 表示某个模块下的某个特定权限。如 read,write 等
12+
accessMap[accessNameList[1]] = (accessMap[accessNameList[1]] || []).concat(
13+
accessNameList[2]
14+
)
15+
return accessMap
16+
}, {})
17+
}
18+
19+
/**
20+
* @description 根据重建的当前用户各个模块权限的 hashmap 过滤所有私有路由,得到当前用
21+
* 户可访问的私有路由
22+
* @param {Array} routes 所有的私有路由表
23+
* @param {Object} accessMap 一个对应当前用户各个模块权限的 hashmap
24+
*/
25+
function filterRoutes (routes, accessMap) {
26+
return routes.reduce((filteredRoutes, route) => {
27+
const routeCopy = { ...route } // shallow copy
28+
// 2.1 validate private route access
29+
// determine whether private route is added to global routes map.
30+
if (validateAccess(route, accessMap)) {
31+
if (routeCopy.children) {
32+
// recursive filter
33+
routeCopy.children = filterRoutes(routeCopy.children, accessMap)
34+
}
35+
36+
if (!(routeCopy.children && !routeCopy.children.length)) {
37+
filteredRoutes.push(routeCopy)
38+
}
39+
}
40+
return filteredRoutes
41+
}, [])
42+
}
43+
44+
/**
45+
* @description 验证用户是否有权限访问某个私有路由
46+
* @param {Object} route 某个私有路由对象
47+
* @param {Array} accessMap 一个对应当前用户各个模块权限的 hashmap
48+
*/
49+
// ! 本质问题: 如何确定两个数组是否存在交集
50+
function validateAccess (route, accessMap) {
51+
if (route.meta && route.meta.access) {
52+
return Object.keys(route.meta.access).every(accessName => {
53+
// 在后端回传的权限表中不存在对应 route 所需的最低权限中的一项,
54+
// 即不满足 private route 的最低权限需求
55+
if (!accessMap[accessName]) return false
56+
57+
return route.meta.access[accessName].every(routeAccess =>
58+
accessMap[accessName].includes(routeAccess)
59+
)
60+
})
61+
}
62+
63+
// situation: preset private route has no 'access' field
64+
return true
65+
}
66+
67+
export default function createPrivateRoutes (accessMap) {
68+
return filterRoutes(routes, accessMap)
69+
}

src/permission/index.js

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,21 @@ import NProgress from 'nprogress'
55
import 'nprogress/nprogress.css'
66
import { tokenFromStorage, userInfoFromStorage } from 'UTILS/storage'
77
import { MessageBox } from 'element-ui'
8-
import createDynamicRoutes from './create-routes'
8+
// import createDynamicRoutes from './create-routes'
9+
import createDynamicRoutes from './filter-routes'
910
import constantRoutes from 'ROUTER/routes/constant'
1011

1112
NProgress.configure({ showSpinner: false })
1213

13-
const WHITELIST = [
14-
'/login'
15-
]
14+
const WHITELIST = ['/login']
1615

1716
// Used to determine whether private routes has been added
1817
let HAS_ROUTES_ADDED = false
1918

20-
function setDynamicRoutesToStorage (roles) {
21-
const currentRole = Array.isArray(roles) ? roles[0] : roles
19+
function setDynamicRoutesToStorage () {
2220
store.commit(
2321
`login/${loginTypes.SET_DYNAMIC_ROUTES}`,
24-
createDynamicRoutes(currentRole)
22+
createDynamicRoutes(store.getters['login/accessMap'])
2523
)
2624
}
2725

@@ -40,24 +38,27 @@ function errorHandler (e, next, redirectPath) {
4038
showClose: false
4139
})
4240
.then(() => store.dispatch('login/userLogout'))
43-
.then(() => next({
44-
path: `/login?redirect=${redirectPath}`,
45-
replace: true
46-
}))
41+
.then(() =>
42+
next({
43+
path: `/login?redirect=${redirectPath}`,
44+
replace: true
45+
})
46+
)
4747
NProgress.done()
4848
console.error(e)
4949
}
5050

5151
function addRoutesToRouter () {
5252
router.addRoutes(store.getters['login/dynamicRoutes'])
5353
console.log(
54-
'%c[Routes creation]: routes has been added!', 'color: yellow',
54+
'%c[Routes creation]: routes has been added!',
55+
'color: yellow',
5556
store.getters['login/dynamicRoutes']
5657
)
5758
}
5859

5960
function createRoutesMap (to, next) {
60-
setDynamicRoutesToStorage(store.getters['login/role'])
61+
setDynamicRoutesToStorage()
6162
setGlobalRoutesToStorage()
6263
addRoutesToRouter()
6364
HAS_ROUTES_ADDED = true
@@ -80,16 +81,13 @@ router.beforeEach((to, from, next) => {
8081

8182
// ! State: User has been logged in (local token).
8283
if (tokenFromStorage.getItem()) {
83-
// 1. fetch RolesMap if necessary (when RolesMap stored by back-end)
84-
85-
// local storage has a accessToken record, but current `login/accessToken`
84+
// (Optional) local storage has a accessToken record, but current `login/accessToken`
8685
// vuex state is empty string when user activate a new session (eg. new
8786
// browser tab)
87+
8888
if (!store.getters['login/accessToken']) {
89-
return store.dispatch(
90-
'login/fetchUserAccess',
91-
tokenFromStorage.getItem()
92-
)
89+
return store
90+
.dispatch('login/fetchUserAccess', tokenFromStorage.getItem())
9391
.then(() => {
9492
// fill vuex state with user information to prevent infinity loop.
9593
store.commit(
@@ -105,38 +103,38 @@ router.beforeEach((to, from, next) => {
105103
.catch(e => errorHandler(e, next, to.path))
106104
}
107105

108-
// 2. confirm route access by user role, including global routes creation.
109-
if (!store.getters['login/role']) {
106+
// step: 1. confirm route access by user access, including global routes creation.
107+
if (!store.getters['login/accesses'].length) {
110108
// 2.1 fetch user role, then routes creation based on user role.
111-
return store.dispatch(
112-
'login/fetchUserAccess',
113-
tokenFromStorage.getItem()
114-
)
109+
return store
110+
.dispatch('login/fetchUserAccess', tokenFromStorage.getItem())
115111
.then(() => createRoutesMap(to, next))
116112
.catch(e => errorHandler(e, next, to.path))
117113
}
118114

119-
// (2.2 optional) Regenerate private routes based on user role when page
115+
// (step 1.2 optional) Regenerate private routes based on user role when page
120116
// reload, because vuex state will be preserved by vuex-persistedstate when
121117
// page reload.
122-
if (store.getters['login/role'] && !HAS_ROUTES_ADDED) {
118+
if (store.getters['login/accesses'].length && !HAS_ROUTES_ADDED) {
123119
console.log(
124120
'%c[Routes creation]: Activate private routes regeneration process !',
125121
'color: yellow;'
126122
)
127123
return createRoutesMap(to, next)
128124
}
129125

130-
// 2.2 filter route
131-
if (!to.meta.roles ||
132-
to.meta.roles.includes(store.getters['login/role'][0])) {
133-
next()
134-
} else {
135-
next({
136-
path: `/403?redirect=${to.path}`,
137-
replace: true
138-
})
139-
}
126+
next()
127+
128+
// step: real-time routes filter
129+
// ! 如有动态权限验证的需求
130+
// if (!to.meta.access || validateAccess(route, to.meta.access)) {
131+
// next()
132+
// } else {
133+
// next({
134+
// path: `/403?redirect=${to.path}`,
135+
// replace: true
136+
// })
137+
// }
140138
} else {
141139
// ! State: user logout
142140
next({

0 commit comments

Comments
 (0)