momo's Blog.

手把手教你将vue-admin-template的路由改造为后端动态路由

字数统计: 2.9k阅读时长: 15 min
2021/06/10 Share

前言

在个人开发中的前后端分离项目, 一般来说并没有前端来配置权限的需求, 所以我们就需要去配置后端路由.

前期准备

下载代码

1
2
3
4
5
6
7
wget https://codeload.github.com/PanJiaChen/vue-admin-template/zip/refs/tags/4.4.0
unzip vue-admin-template-4.4.0.zip

cd vue-admin-template-4.4.0

npm i
npm run dev

关于后端动态路由,作者是这样说的

但其实很多公司的业务逻辑可能不是这样的,举一个例子来说,很多公司的需求是每个页面的权限是动态配置的,不像本项目中是写死预设的。但其实原理是相同的。如:你可以在后台通过一个 tree 控件或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。当用户登录后得到 roles,前端根据roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上,你会发现原理是相同的,万变不离其宗。

1
2
3
4
5
6
7
8
9
10
const map={
login:require('login/index').default // 同步的方式
login:()=>import('login/index') // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
{ path: '/login', component: 'login', hidden: true }
]
//之后遍历这个map,动态生成asyncRoutes
并将 component 替换为 map[component]

创建本地组件的路由映射表

我们在 src/router/index.js 中添加

1
2
3
4
5
6
export const routerMap = {
Layout: () => import('@/layout'),
Dashboard: () => import('@/views/dashboard/index'),
Table: () => import('@/views/table/index'),
Tree: () => import('@/views/tree/index')
}

并且删除无关的路由 只需要默认登录页和404页面

router.js

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
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},

{
path: '/404',
component: () => import('@/views/404'),
hidden: true
}
]

export const routerMap = {
Layout: () => import('@/layout'),
Dashboard: () => import('@/views/dashboard/index'),
Table: () => import('@/views/table/index'),
Tree: () => import('@/views/tree/index')
}

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

定义接口

此次我们使用mock生成假数据

将user接口数据稍微修改一下

/mock/user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const users = {
'admin-token': {
// 不返回数组, 返回指定的role,或者role_id
role: 'admin',
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
role: 'editor',
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}

新建:/mock/component.js

后端接口返回的数据结构需要和前端定义的一样, 只不过把component换成我们的router map 的key,格式请参考下方.

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

module.exports = [
{
url: '/vue-admin-template/component/list\.*',
type: 'post',
response: config => {
const { role } = config.body
let data = {}
if (role === 'admin') {
data = {
code: 20000,
data: {
role_name: 'ops',
component_list: [
{
path: '/',
component: 'Layout',
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: 'Dashboard',
meta: { title: 'Dashboard', icon: 'dashboard' }
}
]
},
{
path: '/example',
component: 'Layout',
redirect: '/example/table',
name: 'Example',
meta: { title: 'Example', icon: 'el-icon-s-help' },
children: [
{
path: 'table',
name: 'Table',
component: 'Table',
meta: { title: 'Table', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: 'Tree',
meta: { title: 'Tree', icon: 'tree' }
}
]
}
]
}
}
}
return data
}
}
]

新建/src/api/component.js

添加接口

1
2
3
4
5
6
7
8
9
import request from '@/utils/request'

export function getList(data) {
return request({
url: '/vue-admin-template/component/list',
method: 'post',
data
})
}

更改路由权限的逻辑

修改用户的数据接口

因为我们有了role来区分,所以我们需要修改../store/modules/user.js

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
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
// 新增
role: ''
}
}


const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
// 新增
SET_ROLE: (state, role) => {
state.role = role
}
}


// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}

// 新增
const { name, avatar, role } = data

commit('SET_NAME', name)
commit('SET_AVATAR', avatar)

// 新增
commit('SET_ROLE', role)
resolve(data)
}).catch(error => {
reject(error)
})
})
},

添加getters.js

1
2
3
4
5
6
7
8
9
10
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
// 新增
role: state => state.user.role
}
export default getters

我们的template的路由守卫并没有写关于权限控制的逻辑,所以我们需要将 vue-template-admin/src/permission.js 替换过来,并且稍微修改一些地方。

需要修改的地方在下面代码有标注

/src/permission.js

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
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()

// set page title
document.title = getPageTitle(to.meta.title)

// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
} else {
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.role && store.getters.role.length > 0
// const hasRoles = true
if (hasRoles) {
next()
} else {
try {
// get user info
// 这里返回的数据, 不在要求是数组, 返回字符串或者ID即可
const { role } = await store.dispatch('user/getInfo')
// generate accessible routes map based on roles
// 这里参数修改为上面返回值
const accessRoutes = await store.dispatch('permission/generateRoutes', role)

// dynamically add accessible routes
router.addRoutes(accessRoutes)

// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
next({ ...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/

if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})

router.afterEach(() => {
// finish progress bar
NProgress.done()
})

上述代码,我们主要关注这一段

1
2
3
4
5
6
7
8
// 拿到用户角色
const { role } = await store.dispatch('user/getInfo')

// 通过角色去过滤出符合条件的router
const accessRoutes = await store.dispatch('permission/generateRoutes', role)

// 将路由动态挂载
router.addRoutes(accessRoutes)

其中,permission/generateRoutes这个 action 我们也需要copy过来.

/src/store/modules/permission.js

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
import { asyncRoutes, constantRoutes } from '@/router'

/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}

/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = []

routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})

return res
}

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

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

const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}

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

/src/store/index.js 添加 permission

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/permission'

Vue.use(Vuex)

const store = new Vuex.Store({
modules: {
app,
settings,
user,
permission
},
getters
})

export default store

修改权限验证

前端权限验证

首先让我们先看一下原本的验证逻辑

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
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}

export function filterAsyncRoutes(routes, roles) {
const res = []

routes.forEach(route => {
const tmp = { ...route }
// 判断是否有权限
if (hasPermission(roles, tmp)) {
if (tmp.children) {
// 如果有children,则新建children属性接收递归的返回值
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
// 返回
return res
}


const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
// 返回过滤过的路由配置
resolve(accessedRoutes)
})
}
}

通过递归, 加上权限验证, 最后返回一个用户有权限的router配置.

这个是一个前端配置, 因为 asyncRoutes 这个数组是前端配置的, 我们需要将它改造为后端数据.

更改为后端权限验证

首先我们需要修改主要的方法

1
2
3
4
5
6
7
8
9
10
11
12
const actions = {
generateRoutes({ commit }, role) {
return new Promise(resolve => {
// 从接口中获取数据
getList(role).then(Response => {
// 处理逻辑
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
})
}
}

拿到数据以后, 我们需要将本地组件和后端接口返回的数据进行映射

1
2
3
4
5
6
7
8
9
10
// 此方法通过递归的方式依次去映射前后端对应的组件实例
function generateAsyncRouter(routerMap, serverRouterMap) {
serverRouterMap.forEach(function(item, index) {
item.component = routerMap[item.component]
if (item.children && item.children.length > 0) {
generateAsyncRouter(routerMap, item.children)
}
})
return serverRouterMap
}

导入前端的路由映射
import { constantoutes, routerMap } from '@/router'

完成generateRoutes处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const actions = {
generateRoutes({ commit }, role) {
return new Promise(resolve => {
getList({ role: role }).then(Response => {
const accessedRoutes = generateAsyncRouter(routerMap, Response.data.component_list)
// 这里需要在数组最后添加一个404页面,并且删除静态配置的404页面
accessedRoutes.push({ path: '*', redirect: '/404', hidden: true })
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
})
}
}

修改前端sidebar逻辑

上述操作修改以后, 我们会发现并没有生效, 因为我们前端的组件还需要改一些地方.
src\layout\components\Sidebar\index.vue

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
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
// 从permission_routes中循环生成左侧导航栏
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar',
'permission_routes'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>

上面我们从vuex中拿到了permission_routes 数据, 并且循环生成导航栏. 但是我们并没有这个数组,所以我们调整一下.

在vuex中添加后端获得的路由数据 permission_routes

src\store\getters.js

1
2
3
4
5
6
7
8
9
10
11
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
role: state => state.user.role,
// 新增
permission_routes: state => state.permission.routes
}
export default getters

完整 permission.js

src\store\modules\permission.js

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
import { constantRoutes, routerMap } from '@/router'
import { getList } from '@/api/component'

function generateAsyncRouter(routerMap, serverRouterMap) {
serverRouterMap.forEach(function(item, index) {
item.component = routerMap[item.component]
if (item.children && item.children.length > 0) {
generateAsyncRouter(routerMap, item.children)
}
})
return serverRouterMap
}

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

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

const actions = {
generateRoutes({ commit }, role) {
return new Promise(resolve => {
getList({ role: role }).then(Response => {
const accessedRoutes = generateAsyncRouter(routerMap, Response.data.component_list)
accessedRoutes.push({ path: '*', redirect: '/404', hidden: true })
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
})
}
}

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

讨论

这样的话, 有细心小伙伴可能会发现, 如果我们提前知道了某些组件的映射关系, 那么是不是可以通过伪造响应来达到访问没有权限的页面呢?

所以, 如果后端控制权限, 就一定要将完整的权限控制应用到接口范围, 每次接口请求都需要鉴权才行.

参考代码

https://github.com/momommm/vue-admin-template-demo

参考文档

CATALOG
  1. 1. 前言
  2. 2. 前期准备
    1. 2.1. 下载代码
    2. 2.2. 创建本地组件的路由映射表
    3. 2.3. 定义接口
  3. 3. 更改路由权限的逻辑
    1. 3.1. 修改用户的数据接口
  4. 4. 修改权限验证
    1. 4.1. 前端权限验证
    2. 4.2. 更改为后端权限验证
      1. 4.2.1. 完成generateRoutes处理函数
  5. 5. 修改前端sidebar逻辑
    1. 5.1. 在vuex中添加后端获得的路由数据 permission_routes
    2. 5.2. 完整 permission.js
  6. 6. 讨论
  7. 7. 参考代码
  8. 8. 参考文档