• 一文搞懂《前后端动态路由权限》


    前言

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

    • 登录:当用户填写完账号和密码后向服务器验证是否正确,验证通过后服务端会返回一个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目录下新建如下页面,用于我们后台管理的主体。

    注意:由于页面主体部分是显示路由的主要入口,因此我们需要在这个页面上使用来显示路由页面

    基础路由(不需要权限的路由的定义)

    我这里只是举例说明哈,所以我这里只是定义了两个不需要权限的路由,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. <script>
    2. // import store from '@/store'
    3. import MenuItem from './MenuItem.vue'
    4. export default {
    5. name:'SiderBar',
    6. data(){
    7. return{
    8. }
    9. },
    10. components:{
    11. MenuItem
    12. },
    13. mounted(){
    14. // console.log('嘿嘿',this.$store.state.userRouters)
    15. }
    16. }
    17. script>
    18. <style>
    19. .side-bar{
    20. position: fixed;
    21. width: 200px;
    22. left: 0;
    23. top: 0;
    24. height: 100%;
    25. /* 因为上面头部60,然后需要给个内边距 */
    26. padding-top:90px;
    27. z-index: 1;
    28. box-sizing: border-box;
    29. background-color: #ededed;
    30. }
    31. style>

    MenuItem.vue页面主要如下 

    1. <template>
    2. <div>
    3. <ul v-if="item.children && item.children.length>0">
    4. <li>
    5. <router-link :to="item.link || item.path">{{item.title}}router-link>
    6. <template v-for="(v,i) in item.children">
    7. <MenuItem :item="v" :key="i">MenuItem>
    8. template>
    9. li>
    10. ul>
    11. <ul v-else>
    12. <router-link :to="item.link || item.path">{{item.title}}router-link>
    13. ul>
    14. div>
    15. template>
    16. <script>
    17. export default {
    18. name:'MenuItem',
    19. data(){
    20. return {
    21. }
    22. },
    23. props:{
    24. item:Object
    25. },
    26. }
    27. script>
    28. <style lang="" scoped>
    29. 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],对应如下:

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

  • 相关阅读:
    《视觉 SLAM 十四讲》V2 第 9 讲 后端优化1 【扩展卡尔曼滤波器 EKF && BA+非线性优化(Ceres、g2o)】
    《ClickHouse原理解析与应用实践》读书笔记(3)
    营收、利润双增长,龙湖集团找到多元增长的答案?
    Git的基本使用
    LeetCode--回文数
    Nacos 认识和安装-1
    贪心算法解决批量开票限额的问题
    【智慧排水】排水管网水位怎么监测
    Orange Pi i96 入手填坑问题总结
    day4.python基础下
  • 原文地址:https://blog.csdn.net/weixin_46872121/article/details/127948420