前言 在个人开发中的前后端分离项目, 一般来说并没有前端来配置权限的需求, 所以我们就需要去配置后端路由.
前期准备 下载代码 1 2 3 4 5 6 7 wget https: unzip vue-admin-template-4.4.0.zip cd vue-admin-template-4.4.0npm 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' ) } const serviceMap=[ { path: '/login' , component: 'login' , hidden: true } ] 并将 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://gi thub.com/vuejs/vue-router/issues/1234 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: 'admin', introduction: 'I am a super administrator', avatar: 'https://wpimg.wallstcn.com/f778738 c-e4f8-4870 -b634-5670 3b4acafe.gif', name: 'Super Admin' }, 'editor-token': { role: 'editor', introduction: 'I am an editor', avatar: 'https://wpimg.wallstcn.com/f778738 c-e4f8-4870 -b634-5670 3b4acafe.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' import 'nprogress/nprogress.css' import { getToken } from '@/utils/auth' import getPageTitle from '@/utils/get-page-title' NProgress.configure({ showSpinner : false }) const whiteList = ['/login' , '/auth-redirect' ] router.beforeEach(async (to, from , next) => { NProgress.start() document .title = getPageTitle(to.meta.title) const hasToken = getToken() if (hasToken) { if (to.path === '/login' ) { next({ path : '/' }) NProgress.done() } else { const hasRoles = store.getters.role && store.getters.role.length > 0 if (hasRoles) { next() } else { try { const { role } = await store.dispatch('user/getInfo' ) const accessRoutes = await store.dispatch('permission/generateRoutes' , role) router.addRoutes(accessRoutes) next({ ...to, replace : true }) } catch (error) { await store.dispatch('user/resetToken' ) Message.error(error || 'Has Error' ) next(`/login?redirect=${to.path} ` ) NProgress.done() } } } } else { if (whiteList.indexOf(to.path) !== -1 ) { next() } else { next(`/login?redirect=${to.path} ` ) NProgress.done() } } }) router.afterEach(() => { NProgress.done() })
上述代码,我们主要关注这一段
1 2 3 4 5 6 7 8 const { role } = await store.dispatch('user/getInfo' )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' 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) { 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) { 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) accessedRoutes.push({ path : '*' , redirect : '/404' , hidden : true }) commit('SET_ROUTES' , accessedRoutes) resolve(accessedRoutes) }) }) } }
上述操作修改以后, 我们会发现并没有生效, 因为我们前端的组件还需要改一些地方.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 (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
参考文档