前言

在开发管理后台时,都会存在多个角色登录,登录成功后,不同的角色会展示不同的菜单路由。这就是我们通常所说的动态路由权限,实现路由权限的方案有多种,比较常用的是由前端使用addRoutes(V3版本改成了addRoute)动态挂载路由和服务端返回可访问的路由菜单这两种。今天主要是从前端角度,实现路由权限的功能。

RBAC模型

前端实现路由权限主要是基于RBAC模型。

RBAC(Role-Based Access Control)即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。

代码实现

登录

首先是登录,登录成功后,服务端会返回用户登录的角色、token以及用户信息等。用户角色如:role: ['admin']。我们一般会将这些信息保存到Vuex里。

const login = () => {
  ruleFormRef.value?.validate((valid: boolean) => {
    if (valid) {
      store.dispatch('userModule/login', { ...accountForm })
    } else {
      console.log('error submit!')
    }
  })
}

信息存储在Vuex:

async login({ commit }, payload: IRequest) {
  // 登录获取token
  const { data } = await accountLogin(payload)
  commit('SET_TOKEN', data.token)
  localCache.setCache('token', data.token)
  // 获取用户信息
  const userInfo = await getUserInfo(data.id)
  commit('SET_USERINFO', userInfo.data)
  localCache.setCache('userInfo', userInfo.data)
  router.replace('/')
},

服务端返回token:

服务端返回用户信息:

菜单信息

路由菜单信息分为两种,一种是默认路由constantRoutes,即所有人都能够访问的页面,不需去通过用户角色去判断,如login、404、首页等等。还有一种就是动态路由asyncRoutes,用来放置有权限(roles 属性)的路由,这部分的路由是需要访问权限的。我们最终将在动态路由里面根据用户角色筛选出能访问的动态路由列表。

我们将默认路由和动态路由都写在router/index.ts里。

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const Layout = () => import('@/Layout')

/** 常驻路由 */
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index.vue'),
    meta: {
      title: '登录',
      hidden: true
    }
  },
  {
    path: '/',
    component: Layout,
    redirect: '/analysis/dashboard',
    name: 'Analysis',
    meta: {
      hidden: false,
      icon: 'icon-home',
      title: '系统总览'
    },
    children: [
      {
        path: '/analysis/dashboard',
        name: 'Dashboard',
        component: () => import('@/views/analysis/dashboard/dashboard.vue'),
        meta: { title: '商品统计', hidden: false }
      },
      {
        path: '/analysis/overview',
        name: 'Overview',
        component: () => import('@/views/analysis/overview/overview.vue'),
        meta: { title: '核心技术', hidden: false }
      }
    ]
  },
  {
    path: '/product',
    component: Layout,
    redirect: '/product/category',
    name: 'Product',
    meta: {
      hidden: false,
      icon: 'icon-tuijian',
      title: '商品中心'
    },
    children: [
      {
        path: '/product/category',
        name: 'Category',
        component: () => import('@/views/product/category/category.vue'),
        meta: { title: '商品类别', hidden: false }
      },
      {
        path: '/product/goods',
        name: 'Goods',
        component: () => import('@/views/product/goods/goods.vue'),
        meta: { title: '商品信息', hidden: false }
      }
    ]
  },
  {
    path: '/story',
    component: Layout,
    redirect: '/story/chat',
    name: 'Story',
    meta: {
      hidden: false,
      icon: 'icon-xiaoxi',
      title: '随便聊聊'
    },
    children: [
      {
        path: '/story/chat',
        name: 'Story',
        component: () => import('@/views/story/chat/chat.vue'),
        meta: { title: '你的故事', hidden: false }
      },
      {
        path: '/story/list',
        name: 'List',
        component: () => import('@/views/story/list/list.vue'),
        meta: { title: '故事列表', hidden: false }
      }
    ]
  },
  {
    path: '/404',
    component: () => import('@/views/404.vue'),
    meta: {
      title: 'Not Found',
      hidden: true
    }
  },
  {
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    meta: {
      hidden: true,
      title: 'Not Found'
    }
  }
]
/**
 * 动态路由
 * 用来放置有权限(roles 属性)的路由
 * 必须带有 name 属性
 */
export const asyncRoutes: RouteRecordRaw[] = [
  {
    path: '/system',
    component: Layout,
    redirect: '/system/department',
    name: 'System',
    meta: {
      hidden: false,
      icon: 'icon-shezhi',
      title: '系统管理'
    },
    children: [
      {
        path: '/system/department',
        name: 'Department',
        component: () => import('@/views/system/department/department.vue'),
        meta: { title: '部门管理', hidden: false, role: ['admin'] }
      },
      {
        path: '/system/menu',
        name: 'Menu',
        component: () => import('@/views/system/menu/menu.vue'),
        meta: { title: '菜单管理', hidden: false, role: ['admin'] }
      },
      {
        path: '/system/role',
        name: 'Role',
        component: () => import('@/views/system/role/role.vue'),
        meta: { title: '角色管理', hidden: false, role: ['editor'] }
      },
      {
        path: '/system/user',
        name: 'User',
        component: () => import('@/views/system/user/user.vue'),
        meta: { title: '用户管理', hidden: false, role: ['editor'] }
      }
    ]
  }
]
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes
})
export default router

我们将系统管理这个菜单作为动态路由部分,里面的子菜单meta属性下都分配有一个访问权限的role属性,我们需要将role属性和用户角色去匹配是否用户具有访问权限。

动态路由筛选

思路:

我们登录得到了用户角色role和写好路由信息(分为默认路由列表和动态路由列表),之后我们需要做的就是通过用户角色role去匹配动态路由列表里面每个子路由的role属性,得到能够访问的动态路由部分,将默认路由和我们得到的动态路由进行拼接这样我们就得到了用户能够访问的完整前端路由,最后使用addRoute将完整路由挂载到router上。

有了这样一个比较清晰的思路,接下来我们就来尝试着实现它。

我们可以将这块的逻辑也放在Vuex里面,在store/modules下新建一个permission.ts文件。

首先我们需要写一个方法去判断用户是否具有访问单个路由的权限:

/**
 * 判断用户是否有权限访问单个路由
 * roles:用户角色
 * route:访问的路由
 */
const hasPermission = (roles: string[], route: any) => {
  if (route.meta && route.meta.roles) {
    return roles.some((role) => {
      if (route.meta?.roles !== undefined) {
        return route.meta.roles.includes(role)
      } else {
        return false
      }
    })
  } else {
    return true
  }
}

实现的核心是route.meta.roles.includes(role),即路由的roles是否包含了用户的角色,包含了就可以访问,否则不能。

对用户角色进行some遍历主要是用户的角色可能存在多个,如:['admin', 'editor']。

这样我们就实现了单个路由访问权限的筛选,但是动态路由列表是一个数组,每个一级路由下可能有二级路由、三级路由甚至更多,这样我们就需要用到递归函数进行筛选:

/**
 * 筛选可访问的动态路由
 * roles:用户角色
 * route:访问的动态列表
 */
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
  const res: RouteRecordRaw[] = []
  routes.forEach((route) => {
    const r = { ...route }
    if (hasPermission(roles, r)) {
      if (r.children) {
        r.children = filterAsyncRoutes(r.children, roles)
      }
      res.push(r)
    }
  })
  return res
}

这样,通过调用filterAsyncRoutes这个函数,然后传入utes:动态路由列表,roles:用户角色两个参数就能得到我们能访问的动态路由了。

然后我们将筛选得到的动态路由和默认路由通过concat拼接得到完整可访问路由,最后通过addRoute挂载。

我们将以上代码逻辑整理到sion.ts里:

import { Module } from 'vuex'
import { RouteRecordRaw } from 'vue-router'
import { constantRoutes, asyncRoutes } from '@/router'
import { IRootState } from '../types'
import router from '@/router'
/**
 * 判断用户是否有权限访问单个路由
 * roles:用户角色
 * route:访问的路由
 */
const hasPermission = (roles: string[], route: any) => {
  if (route.meta && route.meta.roles) {
    return roles.some((role) => {
      if (route.meta?.roles !== undefined) {
        return route.meta.roles.includes(role)
      } else {
        return false
      }
    })
  } else {
    return true
  }
}
/**
 * 筛选可访问的动态路由
 * roles:用户角色
 * route:访问的动态列表
 */
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
  const res: RouteRecordRaw[] = []
  routes.forEach((route) => {
    const r = { ...route }
    if (hasPermission(roles, r)) {
      if (r.children) {
        r.children = filterAsyncRoutes(r.children, roles)
      }
      res.push(r)
    }
  })
  return res
}
interface IPermissionState {
  routes: RouteRecordRaw[]
  dynamicRoutes: RouteRecordRaw[]
}
export const routesModule: Module<IPermissionState, IRootState> = {
  namespaced: true,
  state: {
    routes: [],
    dynamicRoutes: []
  },
  getters: {},
  mutations: {
    SET_ROUTES(state, routes) {
      state.routes = routes
    },
    SET_DYNAMICROUTES(state, routes) {
      state.dynamicRoutes = routes
    }
  },
  actions: {
    generateRoutes({ commit }, { roles }) {
      // accessedRoutes: 筛选出的动态路由
      const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      // 将accessedRoutes和默认路由constantRoutes拼接得到完整可访问路由
      commit('SET_ROUTES', constantRoutes.concat(accessedRoutes))
      commit('SET_DYNAMICROUTES', accessedRoutes)
      // 通过addRoute将路由挂载到router上
      accessedRoutes.forEach((route) => {
        router.addRoute(route)
      })
    }
  }
}

这样就实现了所有代码逻辑。有个问题,addRoute应该何时调用,在哪里调用?

登录后,获取用户的权限信息,然后筛选有权限访问的路由,再调用addRoute添加路由。这个方法是可行的。但是不可能每次进入应用都需要登录,用户刷新浏览器又要登录一次。所以addRoute还是要在全局路由守卫里进行调用。

我们在router文件夹下创建一个permission.ts,用于写全局路由守卫相关逻辑:

import router from '@/router'
import { RouteLocationNormalized } from 'vue-router'
import localCache from '@/utils/cache'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import store from '@/store'

NProgress.configure({ showSpinner: false })
const whiteList = ['/login']
router.beforeEach(
  async (
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    next: any
  ) => {
    document.title = to.meta.title as string
    const token: string = localCache.getCache('token')
    NProgress.start()
    // 判断该用户是否登录
    if (token) {
      if (to.path === '/login') {
        // 如果登录,并准备进入 login 页面,则重定向到主页
        next({ path: '/' })
        NProgress.done()
      } else {
        const roles = store.state.userModule.roles
        store.dispatch('routesModule/generateRoutes', { roles })
        // 确保添加路由已完成
        // 设置 replace: true, 因此导航将不会留下历史记录
        next({ ...to, replace: true })
        // next()
      }
    } else {
      // 如果没有 token
      if (whiteList.includes(to.path)) {
        // 如果在免登录的白名单中,则直接进入
        next()
      } else {
        // 其他没有访问权限的页面将被重定向到登录页面
        next('/login')
        NProgress.done()
      }
    }
  }
)
router.afterEach(() => {
  NProgress.done()
})

这样,完整的路由权限功能就完成了。我们可以做一下验证:

动态路由

我们登录的用户角色为roles: ['editor'],动态路由为系统管理菜单,里面有四个子路由对应有roles,正常情况下我们可以访问系统管理菜单下的角色管理和用户管理。

渲染菜单界面

筛选出的动态路由

没有任何问题!

总结

前端实现动态路由是基于RBAC思想,通过用户角色去筛选出可以访问的路由挂载在router上。这样实现有一点不好的地方在于菜单信息是写死在前端,以后要改个显示文字或权限信息,需要重新修改然后编译。

到此这篇关于Vue3纯前端实现Vue路由权限的文章就介绍到这了,更多相关Vue3纯前端实现路由权限内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Vue3纯前端实现Vue路由权限的方法详解的更多相关文章

  1. 关闭iOS原生MPVolumeView音频路由菜单

    我正在使用MPVolumeView允许用户在使用我的应用程序时控制他喜欢的音频路径.该代码显示了该视图:当用户点击音频路由按钮时,会出现一个带有可用选项的菜单.问题:显示音量视图的屏幕可能需要隐藏,因为我的应用程序处理各种事件,我想同时隐藏音频路由菜单我的问题:有没有人知道是否可以手动关闭MPVolumeView的音频路由选择菜单而无需用户按下取消按钮?解决方法在iOS8上,您可以使用以下使用私有API的代码

  2. iOS:使用蓝牙音频输出(kAudioSessionProperty_OverrideCategoryEnableBluetoothInput)AudioSession

    >如果有可用的A2DP设备,我的音频路由将始终自动切换到kAudioSessionOutputRoute_BluetoothA2DP路由.如何防止此路线更改?我希望你们中的一些人可以帮助我解决这些问题.这对我对CoreAudio的整体理解,特别是AudioSession框架,真的有帮助.解决方法AudioSession是一项棘手的业务.1.BluetoothHFPaudiooutputisonlypossibleincaseofAudioSessionkAudioSessionCategory_PlayA

  3. Swift3.0 Swift2.3 获取IP地址 获取网关地址

    最近需要在Swift项目中获取路由器的网关地址,在网上找了半天的代码也没发现太多有价值的东西,而且大多都是OC代码,很少有Swift的相关代码,只找到了一个通过Swift代码获取设备IP的代码,最后实在没办法只能曲线救国了。下面上代码:思路就是把获取到的设备IP地址的最后一位手动修改为”1”,前面三位不需要修改,比如我的手机ip地址是192.168.31.212,所以网关地址就是192.168.31.1。最近苹果更新了Swift3.0,这里更新一下代码。

  4. Swift3.0服务端开发(二) 静态文件添加、路由配置以及表单提交

    今天博客中就来聊一下Perfect框架的静态文件的添加与访问,路由的配置以及表单的提交。也就是webroot的文件目录变地方了。后方的尾随闭包是对响应的处理。action的地址就是我们在服务器端配置的路由地址“127.0.0.1:8181/login”,而表单提交的方式是POST。

  5. Swift Web 开发之 Vapor - 路由二

    路由参数Vapor提倡使用类型安全的路由参数来接收数据,我们可以在路由方法中使用Swift类型来指定参数类型,Vapor会在内部解析并将参数返回给闭包以供使用,非常方便。Swift中处处有协议,路由参数也是如此,我们所见例子中的Int其实就是Vapor给实现了StringInitializable协议,当然String也已经默认实现。throw另外一大特性就是可以直接在路由中抛出异常,我们可以throw任何遵从Swift.Error协议的对象,当然Vapor已经为我们封装好了几个常用的Error来方便我们

  6. swift – Singleton模式和正确使用Alamofire的URLRequestConvertible

    如果是这样,我该如何设置经理的基础?此外,如果我使用这个管理器//这可以与上面显示的路由器结构一起工作?我是Alamofire图书馆的新手,迅速。然而,当您获得超过6或7例的情况下,这很快就会变得很快。首先,您的模型对象需要符合RouterObject协议。最后一个问题是您无法直接在Routerenum中存储baseURL或OAuthToken。但是,如果您只是使用默认会话触发网络,那么sharedInstance可能就足够了。

  7. Android VPNService路由排除

    我正在使用OpenVPN和ICS附带的新VpnServiceAPI有没有办法从VPN隧道中定义IP地址的排除?

  8. android – 如何以编程方式在蓝牙和手机之间选择媒体音频

    我需要有一组按钮,其操作类似于JB拨号器中的媒体输出选择器,它将选择“耳机”“扬声器”和“电话听筒”.我可以打开和关闭扬声器,但是在标准手机和蓝牙耳机之间进行媒体输出之间的切换使我望而却步.我如何构建和选项,如控制媒体输出的拨号器?

  9. vue3获取当前路由地址

    本文详细讲解了vue3获取当前路由地址的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  10. 十分钟带你快速上手Vue3过渡动画

    在开发中我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验,下面这篇文章主要给大家介绍了关于如何快速上手Vue3过渡动画的相关资料,需要的朋友可以参考下

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部