当前位置:   article > 正文

一文搞懂《前后端动态路由权限》--后台控制篇_后端为什么要返回路由信息

后端为什么要返回路由信息

前言

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

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

  1. //router.js
  2. module.exports = [
  3. {
  4. id: 2,
  5. pid: 0,
  6. path: '/course',
  7. name: 'Course',
  8. title: '课程管理'
  9. },
  10. {
  11. id: 3,
  12. pid: 2,
  13. path: 'operate',
  14. name: 'CourseOperate',
  15. link: '/course/operate',
  16. title: '课程操作'
  17. },
  18. {
  19. id: 4,
  20. pid: 3,
  21. path: 'info_data',
  22. name: 'CourseInfoData',
  23. link: '/course/operate/info_data',
  24. title: '课程数据'
  25. },
  26. {
  27. id: 5,
  28. pid: 2,
  29. path: 'add',
  30. name: 'CourseAdd',
  31. link: '/course/add',
  32. title: '增加课程'
  33. },
  34. {
  35. id: 6,
  36. pid: 0,
  37. path: '/student',
  38. name: 'Student',
  39. title: '学生管理'
  40. },
  41. {
  42. id: 7,
  43. pid: 6,
  44. path: 'operate',
  45. name: 'StudentOperate',
  46. link: '/student/operate',
  47. title: '学生操作'
  48. },
  49. {
  50. id: 8,
  51. pid: 6,
  52. path: 'add',
  53. name: 'SudentAdd',
  54. link: '/student/add',
  55. title: '增加学生'
  56. },
  57. ];

user.js主要是后天用来根据前台请求中携带的uid进行映射判断有哪些权限的

  1. //user.js
  2. module.exports = [
  3. {
  4. id: 1,
  5. name: 'zhangsan',
  6. auth: [2,3,6,7]
  7. },
  8. {
  9. id: 2,
  10. name: 'lisi',
  11. auth: [2,3,5,6,7,8]
  12. },
  13. {
  14. id: 3,
  15. name: 'wangwu',
  16. auth: [2,3,4,5,6,7,8]
  17. }
  18. ]

前端请求后端路由接口的主要逻辑:

  1. const router = require('koa-router')()
  2. const users = require('../data/user');//存储用户所有用户数据id下对应的权限数据
  3. const routers = require('../data/router');//路由数据
  4. router.post('/user_router_auth', async (ctx, next) => {
  5. // 拿到前端进行该请求时候的参数,主要是uid
  6. const { uid } = ctx.request.body;
  7. // 当uid存在时
  8. if (uid) {
  9. //定义最后向前台返回最后的路由的容器
  10. let authRouterInfo = [];
  11. const userInfo = users.filter(user => user.id == uid)[0];//因为users是一个数组,所以拿返回的数组的第一项(最终过滤出来的项也只会有一个)比如:[2,3,4,5]
  12. //
  13. userInfo.auth.map((rid) => {
  14. // 拿到该uid对应的role数组之后,然后去跟router下的每一项得到id进行遍历判断
  15. routers.map((router) => {
  16. //只有当role中的id与router下的id相等的时候,将该routerpush到authRouterInfo中
  17. if (router.id === rid) {
  18. authRouterInfo.push(router);
  19. }
  20. })
  21. })
  22. ctx.body = authRouterInfo;
  23. } else {
  24. next();
  25. }
  26. })
  27. 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

  1. import Vue from 'vue'
  2. import VueRouter from 'vue-router'
  3. import Home from '@/views/Home.vue'
  4. Vue.use(VueRouter)
  5. const routes = [
  6. {
  7. path: '/',
  8. name:"Home",
  9. component: Home,
  10. },
  11. {
  12. path:'*',
  13. name:'NotFound',
  14. component:()=>import('@/views/NotFound.vue')
  15. }
  16. ]
  17. const router = new VueRouter({
  18. mode: 'history',
  19. base: process.env.BASE_URL,
  20. routes
  21. })
  22. export default router

请求api抽离

在根目录下新建request/index.js文件,用于存储所有后台的请求(再次说明,由于项目比较简单,而且主要是理解动态路由的思想,这里的请求封装我就不进行演示了哈,直接用的axios),具体如下:

  1. import axios from "axios";
  2. import qs from 'qs'
  3. function getUserRouters(uid){
  4. return axios({
  5. // 对应koa服务器下的请求
  6. url:'http://localhost:3000/user_router_auth',
  7. method:'post',
  8. header:{
  9. 'Conten-type':'applicaton/x-wwww-form-urlencoded'
  10. },
  11. data:qs.stringify({uid})
  12. }).then((res)=>{
  13. // 拿到数据直接返回
  14. return res.data
  15. }).catch((err)=>{
  16. throw err;
  17. })
  18. }
  19. export {
  20. getUserRouters
  21. }

Vuex数据存储

在这个demo下,主要是使用store来存储数据的,包括用户uid以及路由,因此需要配置如下文件,

我们都知道,在Vuex中,state主要用来存储数据状态的,mutations是用来改变state数据状态的,actions是用来派发事件的,即是通知mutations进行数据的更新,下面先说明一下state和mutations的具体

 store/index.js内容如下:

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. import state from './state'
  4. import actions from './actions'
  5. import mutations from './mutations'
  6. Vue.use(Vuex)
  7. export default new Vuex.Store({
  8. state,
  9. actions,
  10. mutations
  11. })

由于在这个demo中,我们需要存储到全局的数据有:用户登录之后的id、用户是都有权限的判断、该用户权限下的路由tree,因此在index/state.js中存储数据如下:

  1. export default{
  2. uid:2,//userId
  3. hasAuth:false,//是否有权限
  4. userRouters:[]//权限树
  5. }

这里uid为什么是2呢?我在前文也有提及到,我这里不进行用户登录的演示,这个uid你可以给个初始值null也行,到时候你登录之后将后台返回的id存到这个state.uid下即可

这个demo中我们只需要维护三个state数据状态,因此在mutation中也是对应三个更新state的mutation_type ,store/mutaions.js内容主要如下:

  1. export default{
  2. setUserRouter(state,userRouters){
  3. state.userRouters=userRouters
  4. },
  5. setAuth(state,hasAuth){
  6. state.hasAuth=hasAuth
  7. },
  8. setUid(state,uid){
  9. state.uid=uid
  10. }
  11. }

紧接着是actions,在actions中,我们主要是进行异步请求,向后台请求数据,然后对后台返回的数据进行进一步的处理,然后将处理后的数据commit通知给mutaions进行state更新,进而我们可以通过$store.state.xx访问到最新的state数据状态。

这里主要演示向后台请求路由表数据,然后进行格式化最后通知mutaions进行更新的步骤

一开始,发送请求之后得到的数据,这里的数据是扁平化,我们需要将其格式化为路由Tree的形式:

因此这里封装了格式化后台返回数据的方法lib/util.js

  1. //将后端返回的扁平的结果格式化成树形结构的数据
  2. function formatRouterTree(data){
  3. let parants = data.filter(p=>p.pid==0),
  4. children = data.filter(p=>p.pid!==0)
  5. dataToTree(parants,children)
  6. function dataToTree(parants,children){
  7. // 这里遍历父路由和子路由,对比pid与id
  8. parants.map((p)=>{
  9. children.map((c,i)=>{
  10. if(p.id===c.pid){
  11. // 则证明此时遍历的c则是p的子路由
  12. //这里需要再次递归,因为c下面可能还会有其子路由
  13. //先将children进行深拷贝一份
  14. let _c=JSON.parse(JSON.stringify(children))
  15. //由于c要做父级路由,所以要先将其删除,则_c里面就不会有我原本遍历的东西了
  16. _c.splice(i,1)//当当前的c删除
  17. // 将当前的c作为父级[c]然后与剩下的children作比较
  18. dataToTree([c],_c)
  19. // 若之前已经有添加过children项了,则直接而push
  20. if(p.children){
  21. p.children.push(c)
  22. }else {
  23. // 之前没有添加过,则直接将这个子元素=p.children
  24. // 表示第一次添加
  25. p.children=[c]
  26. }
  27. }
  28. })
  29. })
  30. }
  31. return parants
  32. }

 格式化的结果:

第一个方法formatRouterTree格式化的数据类型如下:

store/actions.js

  1. import {getUserRouters} from '../request/index.js'
  2. import {formatRouterTree} from '../lib/util'
  3. export default{
  4. async setUserRouters({commit,state}){
  5. //根据用户登录之后的uid想后台请求路由数据
  6. const userRouters= await getUserRouters(state.uid)
  7. // 接下来将要处理这个router使得其变为树形结构
  8. const payload=formatRouterTree(userRouters)
  9. console.log('后台返回的数据',generateRouter(payload))
  10. //将格式化后的数据通知mutations进行更新
  11. commit('setUserRouter',payload)
  12. //有上面这个路由tree同时也说明有权限了
  13. commit('setAuth',true)
  14. }
  15. }

经过以上,得到路由tree数据,且你会发现目前为止,还有进行router.addRouters的操作。其中有两个主要原因。一是我们在actions中只对数据进行转成tree的操作,对于router.addRouters()的操作一般是放在permission中,即是路由拦截,但是由于这个demo比较简单,因此这里是先放在main.js中。

在进行router.addRouters()之前,我们也需要封装另外一个方法,即是将上面的路由tree格式化成我们router需要的格式

 第二个方法generateRouter格式化后的数据形式,在ilb/util.js中再添加如下方法:

  1. /**
  2. * 将上面的树形treeRouter改变成我们想要的路由形式,有过滤操作,保留我们想要的
  3. * 最终会被加载到router中:router.addRouters()
  4. * [{path:'',name:'',component:'',children:[{}]}]
  5. *
  6. */
  7. function generateRouter(userRouters){
  8. let newRouter=userRouters.map((item)=>{
  9. let routes={
  10. path:item.path,
  11. name:item.name,
  12. component:()=>import(`@/views/${item.name}`)
  13. }
  14. if(item.children){
  15. routes.children=generateRouter(item.children)
  16. }
  17. //由于递归之后又会返回一个routes,放到children中
  18. return routes
  19. })
  20. return newRouter
  21. }
  22. export{
  23. formatRouterTree,
  24. generateRouter
  25. }

 最后,我们可以在main.js中对我们的路由进行拦截,根据你自己的逻辑去判断进行相应的处理,这个demo主要是首选判断用户有没有hasAuth权限(这里我们在上面actions中已经固定写死了commit('setAuth',true)你可以根据你的业务来定),然后dispatch异步actions获得路由,并在actions中初步格式化,最后拿到这个state中的数据做最后的格式化变成我们想要的路由tree,最后添加到路由表中router.addRoutes(),然后用户就可以在url栏上输入相应的path进行路由访问(当然前提是你有hasAuth权限,如果没有权限这边会给你返回NotFound页面)

  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import router from './router'
  4. import './assets/css/common.css'
  5. import store from './store/index'
  6. import {generateRouter} from './lib/util'
  7. // 路由前卫
  8. router.beforeEach(async (to,form,next)=>{
  9. if(!store.state.hasAuth){
  10. //dispatch action的异步操作,在这异步操作中已经同时commit给mutaions进行更新state的处理
  11. await store.dispatch('setUserRouters')
  12. //接着我们便可以拿到state.userRouters数据进行下一步的格式化,变成我们真正想要的路由tree
  13. const newRoutes=generateRouter(store.state.userRouters);
  14. //这时候可以使用router中的api将其添加到router中
  15. router.addRoutes(newRoutes)
  16. // 有了权限之后,用户点击其相对应的路由则放行
  17. next({path:to.path})
  18. }else{
  19. //无权限的情况下则不用去请求
  20. next()
  21. }
  22. })
  23. Vue.config.productionTip = false
  24. new Vue({
  25. router,
  26. store,
  27. render: h => h(App)
  28. }).$mount('#app')

SiderBar导航设置

侧边栏主要的设置如下,详细的说明也以及注释待代码中,有兴趣可以看一下哈,主要是在MenuItem页面中用到递归的思想

SiderBar.vue页面如下:

  1. <template>
  2. <div class="side-bar">
  3. <!-- 首页不需要权限,所有用户都可以访问 -->
  4. <ul>
  5. <li>
  6. <router-link to="/">首页</router-link>
  7. </li>
  8. </ul>
  9. <!-- 下面则是动态遍历有权限的路由,进行渲染 -->
  10. <template v-for="(item,index) of $store.state.userRouters">
  11. <!--MenuItem会拿到每一个item进行遍历 -->
  12. <MenuItem :item="item" :key="index"> </MenuItem>
  13. </template>
  14. </div>
  15. </template>
  16. <script>
  17. // import store from '@/store'
  18. import MenuItem from './MenuItem.vue'
  19. export default {
  20. name:'SiderBar',
  21. data(){
  22. return{
  23. }
  24. },
  25. components:{
  26. MenuItem
  27. },
  28. mounted(){
  29. // console.log('嘿嘿',this.$store.state.userRouters)
  30. }
  31. }
  32. </script>
  33. <style>
  34. .side-bar{
  35. position: fixed;
  36. width: 200px;
  37. left: 0;
  38. top: 0;
  39. height: 100%;
  40. /* 因为上面头部60,然后需要给个内边距 */
  41. padding-top:90px;
  42. z-index: 1;
  43. box-sizing: border-box;
  44. background-color: #ededed;
  45. }
  46. </style>

MenuItem.vue页面主要如下 

  1. <template>
  2. <div>
  3. <!-- 渲染有children的多级路由 -->
  4. <ul v-if="item.children && item.children.length>0">
  5. <li>
  6. <!-- 这里再判断一层的原因是后端最外层的路由没有返回item.link,那么可以使用path替代,如果有返回则不需要 -->
  7. <router-link :to="item.link || item.path">{{item.title}}</router-link>
  8. <!-- 遍历的思想,再次遍历item下的children -->
  9. <template v-for="(v,i) in item.children">
  10. <MenuItem :item="v" :key="i"></MenuItem>
  11. </template>
  12. </li>
  13. </ul>
  14. <!-- 没有children的路由则直接显示 -->
  15. <ul v-else>
  16. <router-link :to="item.link || item.path">{{item.title}}</router-link>
  17. </ul>
  18. </div>
  19. </template>
  20. <script>
  21. export default {
  22. name:'MenuItem',
  23. data(){
  24. return {
  25. }
  26. },
  27. props:{
  28. item:Object
  29. },
  30. }
  31. </script>
  32. <style lang="" scoped>
  33. </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],对应如下:

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/58991
推荐阅读
相关标签
  

闽ICP备14008679号