一文搞懂《前后端动态路由权限》--后台控制篇

前言

        本文主要针对后台管理系统的权限问题,即不同权限对应着不同的路由,同时侧边栏的路由也需要根据权限的不同异步生成。我们知道,权限那肯定是对应用户的,那么就会涉及到用户登录模块,所以这里也简单说一下实现登录的和权限验证的思路。

  • 登录:当用户填写完账号和密码后向服务器验证是否正确,验证通过后服务端会返回一个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],对应如下:

 以上便是《前后端动态路由权限》的所有内容啦,如果文章中有错误的地方,欢迎大家指正相互讨论与学习,当然,如果对这部分内容源码感兴趣的朋友,也可以滴滴我给你发哈~

猜你喜欢

转载自blog.csdn.net/weixin_46872121/article/details/127948420