前言
本文主要针对后台管理系统的权限问题,即不同权限对应着不同的路由,同时侧边栏的路由也需要根据权限的不同异步生成。我们知道,权限那肯定是对应用户的,那么就会涉及到用户登录模块,所以这里也简单说一下实现登录的和权限验证的思路。
- 登录:当用户填写完账号和密码后向服务器验证是否正确,验证通过后服务端会返回一个token,拿到token之后,前端则会将token保存到本地并且保存到Vuex中(持久化,防止刷新丢失;另外你也可以保存到cookie中,用于记住用户的登录状态),接着前端会根据token再去拉取一个user_info的接口来获取用户的详细信息(如用户权限,用户名等等)
- 权限验证:通过token获取用户对应的role,动态根据用户的role算出其对应的权限路由(这里通常会设计到扁平数据转成路由Tree的操作,因为后端返回的路由数据是扁平的,我们需要经过一定的操作将这个扁平的数据转成我们需要的陆游与Tree格式),然后荣光router.addRoutes动态的挂载这些路由
登录篇
由于这篇文章我主要是想讲讲和权限相关的,所以对于登录的部分详细的过程我这边就此略过哈,我只讲讲大概的过程,具体实现的话网上也会有很多的文章可供参考。
拿到相应用户的role是以用户登录为前提的,因此大致的登录流程如下:
- 提供登录表单(用户名 && 密码),用户登录之后,服务端会返回token(该token是一个能唯一标示用户身份的一个key)
- 将token存储到本地与Vuex中进行持久化操作,同时也将用户的其他详细信息进行合理化的保存(看你个人的编码习惯是存在哪里),这里包括了用户的权限数据role
- 在全局钩子router.beforeEach中拦截路由(目的是路由肯定是用户登录之后,获取到其相应的权限才会有的)
经过以上的登录操作,我们就能拿到指定用户的token、用户role以及用户其他详细信息
权限篇
先说一下我权限控制的主体思路,首先前端会先定义一份通用的路由,比如一些不需要权限控制的路由:Home、404、NotFount等等。然后用户登录之后,拿到用户的的信息,其中就有包括用户uid;前端拿到uid去向后台请求user_router_auth该uid下对应的路由,然后后台会根据这个uid去对比用户表中的数据,去判断该uid下有什么样的的权限,比如uid=2下的role=[3,4,2,5,6],然后这时候后天也会根据这个role去一一对比路由数据库中的id,然后进行过滤,最后返回含有role的路由对象。
后端koa实现逻辑
这里使用的是koa搭建的服务器
简单举例给前端返回的数据router.js
//router.js
module.exports = [
{
id: 2,
pid: 0,
path: '/course',
name: 'Course',
title: '课程管理'
},
{
id: 3,
pid: 2,
path: 'operate',
name: 'CourseOperate',
link: '/course/operate',
title: '课程操作'
},
{
id: 4,
pid: 3,
path: 'info_data',
name: 'CourseInfoData',
link: '/course/operate/info_data',
title: '课程数据'
},
{
id: 5,
pid: 2,
path: 'add',
name: 'CourseAdd',
link: '/course/add',
title: '增加课程'
},
{
id: 6,
pid: 0,
path: '/student',
name: 'Student',
title: '学生管理'
},
{
id: 7,
pid: 6,
path: 'operate',
name: 'StudentOperate',
link: '/student/operate',
title: '学生操作'
},
{
id: 8,
pid: 6,
path: 'add',
name: 'SudentAdd',
link: '/student/add',
title: '增加学生'
},
];
user.js主要是后天用来根据前台请求中携带的uid进行映射判断有哪些权限的
//user.js
module.exports = [
{
id: 1,
name: 'zhangsan',
auth: [2,3,6,7]
},
{
id: 2,
name: 'lisi',
auth: [2,3,5,6,7,8]
},
{
id: 3,
name: 'wangwu',
auth: [2,3,4,5,6,7,8]
}
]
前端请求后端路由接口的主要逻辑:
const router = require('koa-router')()
const users = require('../data/user');//存储用户所有用户数据id下对应的权限数据
const routers = require('../data/router');//路由数据
router.post('/user_router_auth', async (ctx, next) => {
// 拿到前端进行该请求时候的参数,主要是uid
const { uid } = ctx.request.body;
// 当uid存在时
if (uid) {
//定义最后向前台返回最后的路由的容器
let authRouterInfo = [];
const userInfo = users.filter(user => user.id == uid)[0];//因为users是一个数组,所以拿返回的数组的第一项(最终过滤出来的项也只会有一个)比如:[2,3,4,5]
//
userInfo.auth.map((rid) => {
// 拿到该uid对应的role数组之后,然后去跟router下的每一项得到id进行遍历判断
routers.map((router) => {
//只有当role中的id与router下的id相等的时候,将该routerpush到authRouterInfo中
if (router.id === rid) {
authRouterInfo.push(router);
}
})
})
ctx.body = authRouterInfo;
} else {
next();
}
})
module.exports = router
前端实现逻辑分析
这里先大致提一下前端实现的答题流程
- 登录得到用户uid
- 根据uid向后台发起获取路由权限的请求
- 后端返回该uid下对应的路由权限列表(此时是一个json格式)
- 前端这边将这个json进行结构化(原因:后端返回的一般是扁平的数据,没有明确的parent路由和children路由的嵌套tree关系,因此我们需要进行这样的格式化操作)
- 将结构化后的数据再次转成router路由结构(原因:后端返回的数据可能会有比较详细的数据,而路由表中通常只需要用到我们常用的比如path、name、title、component属性)
- 再将结构好的路由结构使用router.addRouers()将其添加到路由表中
- 最后是进行动态的渲染
项目基础搭建
怎么创建项目以及项目所需要的依赖等我就不在这里说明了,大家自行搭建哈。那么我们就进行主题部分的讲解。首先后天管理系统无疑是分为头部、侧边栏(导航项)、页面主体部分,因此我们可以在components目录下新建如下页面,用于我们后台管理的主体。
注意:由于页面主体部分是显示路由的主要入口,因此我们需要在这个页面上使用<router-view>来显示路由页面
基础路由(不需要权限的路由的定义)
我这里只是举例说明哈,所以我这里只是定义了两个不需要权限的路由,Home、NotFound页面,具体如下:router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name:"Home",
component: Home,
},
{
path:'*',
name:'NotFound',
component:()=>import('@/views/NotFound.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
请求api抽离
在根目录下新建request/index.js文件,用于存储所有后台的请求(再次说明,由于项目比较简单,而且主要是理解动态路由的思想,这里的请求封装我就不进行演示了哈,直接用的axios),具体如下:
import axios from "axios";
import qs from 'qs'
function getUserRouters(uid){
return axios({
// 对应koa服务器下的请求
url:'http://localhost:3000/user_router_auth',
method:'post',
header:{
'Conten-type':'applicaton/x-wwww-form-urlencoded'
},
data:qs.stringify({uid})
}).then((res)=>{
// 拿到数据直接返回
return res.data
}).catch((err)=>{
throw err;
})
}
export {
getUserRouters
}
Vuex数据存储
在这个demo下,主要是使用store来存储数据的,包括用户uid以及路由,因此需要配置如下文件,
我们都知道,在Vuex中,state主要用来存储数据状态的,mutations是用来改变state数据状态的,actions是用来派发事件的,即是通知mutations进行数据的更新,下面先说明一下state和mutations的具体
store/index.js内容如下:
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
export default new Vuex.Store({
state,
actions,
mutations
})
由于在这个demo中,我们需要存储到全局的数据有:用户登录之后的id、用户是都有权限的判断、该用户权限下的路由tree,因此在index/state.js中存储数据如下:
export default{
uid:2,//userId
hasAuth:false,//是否有权限
userRouters:[]//权限树
}
这里uid为什么是2呢?我在前文也有提及到,我这里不进行用户登录的演示,这个uid你可以给个初始值null也行,到时候你登录之后将后台返回的id存到这个state.uid下即可
这个demo中我们只需要维护三个state数据状态,因此在mutation中也是对应三个更新state的mutation_type ,store/mutaions.js内容主要如下:
export default{
setUserRouter(state,userRouters){
state.userRouters=userRouters
},
setAuth(state,hasAuth){
state.hasAuth=hasAuth
},
setUid(state,uid){
state.uid=uid
}
}
紧接着是actions,在actions中,我们主要是进行异步请求,向后台请求数据,然后对后台返回的数据进行进一步的处理,然后将处理后的数据commit通知给mutaions进行state更新,进而我们可以通过$store.state.xx访问到最新的state数据状态。
这里主要演示向后台请求路由表数据,然后进行格式化最后通知mutaions进行更新的步骤
一开始,发送请求之后得到的数据,这里的数据是扁平化,我们需要将其格式化为路由Tree的形式:
因此这里封装了格式化后台返回数据的方法lib/util.js
//将后端返回的扁平的结果格式化成树形结构的数据
function formatRouterTree(data){
let parants = data.filter(p=>p.pid==0),
children = data.filter(p=>p.pid!==0)
dataToTree(parants,children)
function dataToTree(parants,children){
// 这里遍历父路由和子路由,对比pid与id
parants.map((p)=>{
children.map((c,i)=>{
if(p.id===c.pid){
// 则证明此时遍历的c则是p的子路由
//这里需要再次递归,因为c下面可能还会有其子路由
//先将children进行深拷贝一份
let _c=JSON.parse(JSON.stringify(children))
//由于c要做父级路由,所以要先将其删除,则_c里面就不会有我原本遍历的东西了
_c.splice(i,1)//当当前的c删除
// 将当前的c作为父级[c]然后与剩下的children作比较
dataToTree([c],_c)
// 若之前已经有添加过children项了,则直接而push
if(p.children){
p.children.push(c)
}else {
// 之前没有添加过,则直接将这个子元素=p.children
// 表示第一次添加
p.children=[c]
}
}
})
})
}
return parants
}
格式化的结果:
第一个方法formatRouterTree格式化的数据类型如下:
store/actions.js
import {getUserRouters} from '../request/index.js'
import {formatRouterTree} from '../lib/util'
export default{
async setUserRouters({commit,state}){
//根据用户登录之后的uid想后台请求路由数据
const userRouters= await getUserRouters(state.uid)
// 接下来将要处理这个router使得其变为树形结构
const payload=formatRouterTree(userRouters)
console.log('后台返回的数据',generateRouter(payload))
//将格式化后的数据通知mutations进行更新
commit('setUserRouter',payload)
//有上面这个路由tree同时也说明有权限了
commit('setAuth',true)
}
}
经过以上,得到路由tree数据,且你会发现目前为止,还有进行router.addRouters的操作。其中有两个主要原因。一是我们在actions中只对数据进行转成tree的操作,对于router.addRouters()的操作一般是放在permission中,即是路由拦截,但是由于这个demo比较简单,因此这里是先放在main.js中。
在进行router.addRouters()之前,我们也需要封装另外一个方法,即是将上面的路由tree格式化成我们router需要的格式
第二个方法generateRouter格式化后的数据形式,在ilb/util.js中再添加如下方法:
/**
* 将上面的树形treeRouter改变成我们想要的路由形式,有过滤操作,保留我们想要的
* 最终会被加载到router中:router.addRouters()
* [{path:'',name:'',component:'',children:[{}]}]
*
*/
function generateRouter(userRouters){
let newRouter=userRouters.map((item)=>{
let routes={
path:item.path,
name:item.name,
component:()=>import(`@/views/${item.name}`)
}
if(item.children){
routes.children=generateRouter(item.children)
}
//由于递归之后又会返回一个routes,放到children中
return routes
})
return newRouter
}
export{
formatRouterTree,
generateRouter
}
最后,我们可以在main.js中对我们的路由进行拦截,根据你自己的逻辑去判断进行相应的处理,这个demo主要是首选判断用户有没有hasAuth权限(这里我们在上面actions中已经固定写死了commit('setAuth',true)你可以根据你的业务来定),然后dispatch异步actions获得路由,并在actions中初步格式化,最后拿到这个state中的数据做最后的格式化变成我们想要的路由tree,最后添加到路由表中router.addRoutes(),然后用户就可以在url栏上输入相应的path进行路由访问(当然前提是你有hasAuth权限,如果没有权限这边会给你返回NotFound页面)
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './assets/css/common.css'
import store from './store/index'
import {generateRouter} from './lib/util'
// 路由前卫
router.beforeEach(async (to,form,next)=>{
if(!store.state.hasAuth){
//dispatch action的异步操作,在这异步操作中已经同时commit给mutaions进行更新state的处理
await store.dispatch('setUserRouters')
//接着我们便可以拿到state.userRouters数据进行下一步的格式化,变成我们真正想要的路由tree
const newRoutes=generateRouter(store.state.userRouters);
//这时候可以使用router中的api将其添加到router中
router.addRoutes(newRoutes)
// 有了权限之后,用户点击其相对应的路由则放行
next({path:to.path})
}else{
//无权限的情况下则不用去请求
next()
}
})
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
SiderBar导航设置
侧边栏主要的设置如下,详细的说明也以及注释待代码中,有兴趣可以看一下哈,主要是在MenuItem页面中用到递归的思想
SiderBar.vue页面如下:
<template>
<div class="side-bar">
<!-- 首页不需要权限,所有用户都可以访问 -->
<ul>
<li>
<router-link to="/">首页</router-link>
</li>
</ul>
<!-- 下面则是动态遍历有权限的路由,进行渲染 -->
<template v-for="(item,index) of $store.state.userRouters">
<!--MenuItem会拿到每一个item进行遍历 -->
<MenuItem :item="item" :key="index"> </MenuItem>
</template>
</div>
</template>
<script>
// import store from '@/store'
import MenuItem from './MenuItem.vue'
export default {
name:'SiderBar',
data(){
return{
}
},
components:{
MenuItem
},
mounted(){
// console.log('嘿嘿',this.$store.state.userRouters)
}
}
</script>
<style>
.side-bar{
position: fixed;
width: 200px;
left: 0;
top: 0;
height: 100%;
/* 因为上面头部60,然后需要给个内边距 */
padding-top:90px;
z-index: 1;
box-sizing: border-box;
background-color: #ededed;
}
</style>
MenuItem.vue页面主要如下
<template>
<div>
<!-- 渲染有children的多级路由 -->
<ul v-if="item.children && item.children.length>0">
<li>
<!-- 这里再判断一层的原因是后端最外层的路由没有返回item.link,那么可以使用path替代,如果有返回则不需要 -->
<router-link :to="item.link || item.path">{{item.title}}</router-link>
<!-- 遍历的思想,再次遍历item下的children -->
<template v-for="(v,i) in item.children">
<MenuItem :item="v" :key="i"></MenuItem>
</template>
</li>
</ul>
<!-- 没有children的路由则直接显示 -->
<ul v-else>
<router-link :to="item.link || item.path">{{item.title}}</router-link>
</ul>
</div>
</template>
<script>
export default {
name:'MenuItem',
data(){
return {
}
},
props:{
item:Object
},
}
</script>
<style lang="" scoped>
</style>
路由页面的创建
这里我需要说明一点哈,路由页面所属目录主要是看你自己是怎么定义的路径的,比如这里是直接统一定在view目录下,想必然这样肯定是不好的,那么你可以自己进行扩展,比如在外面相应放一层父级目录,在该目录下放其所有的子路由页面,你在这里components自己定义好路径即可
效果演示
当uid2=时,其拥有的权限是role:[2,3,5,6,7,8],对应如下:
当uid=3时,其拥有的权限role:[2,3,4,5,6,7,8],对应如下:
当uid=1时,其拥有的权限role:auth: [2,3,6,7],对应如下:
以上便是《前后端动态路由权限》的所有内容啦,如果文章中有错误的地方,欢迎大家指正相互讨论与学习,当然,如果对这部分内容源码感兴趣的朋友,也可以滴滴我给你发哈~