Vue实现角色权限动态路由详细教程,在vue-admin-template基础上修改,附免费完整项目代码

news2025/2/25 15:15:59

前言

vue-admin-template是一个最基础的后台管理模板,只包含了一个后台需要最基础的东西,如果clone的是它的master分支,是没有权限管理的,只有完整版vue-element-admin有这个功能,但是为了小小的一个权限管理而用比较复杂的有点得不偿失。

我在网上找了一堆教程和资料,发现要么说的很乱,要么说的不全,最后连个完整代码都不让我白嫖(bushi)。自己复制粘贴过去都实现不出来,仔细查看发现人家写的教程漏了一写步骤/代码,而且还有bug(服了这些老六)。

在自己摸索了和看了花裤衩大佬的文章后,解决了一些bug自己实现出来了,代码中也有详细注释。完整代码放文末给大家了,大家记得给我star再走(不然小拳拳锤你胸口)。

权限管理?动态路由?

现在开发后台管理系统项目经常有权限管理的需求,权限管理其实就是根据不同的角色权限显示不同的路由,而其中的关键就是动态路由router.addRoutes
实现权限验证的基本思路就是:

  • 用户登录,通过token获取用户对应的 role
  • 动态根据用户的 role 算出其对应有权限的路由
  • 通过 router.addRoutes 动态挂载这些路由

以上步骤实现的核心是routervuex,下面就详细介绍如何实现(按代码执行逻辑倒推

具体实现

  1. 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
  2. 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
  3. 调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。
  4. 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件

本段转载自vue-element-admin的作者:花裤衩

1.router.js路由表

首先我们实现router.js路由表,
一共有两个路由表,

  • 一个是constantRoutes,这个用来放没有权限要求的页面,每个角色都可以访问,比如首页,登录页;
  • 一个是asyncRoutes 动态需要根据权限加载的路由表 。meta里面的roles就存放了页面需要的权限,这些页面只有roles数组里面的角色才能看到。

注意:404一定要放最后面,不然都会重定向到404
src/router/index.js代码如下:

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '@/layout'
/**
 * constantRoutes
 * 没有权限要求的基本页面
 *所有角色都可以访问
 如首页和登录页和一些不用权限的公用页面
 */
export const constantRoutes = [{
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: {
        title: 'Dashboard',
        icon: 'dashboard',

      }
    }]
  },
]
//异步挂载的路由
//动态需要根据权限加载的路由表
export const asyncRoutes = [{
    path: '/example',
    component: Layout,
    redirect: '/example/table',
    name: 'Example',
    alwaysShow: true,
    meta: {
      title: 'Example',
      icon: 'el-icon-s-help',
    },
    children: [{
        path: 'table',
        name: 'Table',
        component: () => import('@/views/table/index'),
        meta: {
          title: 'Table',
          icon: 'table',
          roles: ['editor']
        }
      },
      {
        path: 'tree',
        name: 'Tree',
        component: () => import('@/views/tree/index'),
        meta: {
          title: 'Tree',
          icon: 'tree',
          roles: ['admin', 'editor']
        }
      }
    ]
  },
  {
    path: '/form',
    component: Layout,
    children: [{
      path: 'index',
      name: 'Form',
      component: () => import('@/views/form/index'),
      meta: {
        title: 'Form',
        icon: 'form',
        roles: ['editor']
      }
    }]
  },
  {
    path: '/nested',
    component: Layout,
    redirect: '/nested/menu1',
    alwaysShow: true,
    name: 'Nested',
    meta: {
      title: 'Nested',
      icon: 'nested',
    },
    children: [{
        path: 'menu1',
        component: () => import('@/views/nested/menu1/index'), // Parent router-view
        name: 'Menu1',
        meta: {
          title: 'Menu1',
          roles: ['admin']
        },
        children: [{
            path: 'menu1-1',
            component: () => import('@/views/nested/menu1/menu1-1'),
            name: 'Menu1-1',
            meta: {
              title: 'Menu1-1'
            }
          },
          {
            path: 'menu1-2',
            component: () => import('@/views/nested/menu1/menu1-2'),
            name: 'Menu1-2',
            meta: {
              title: 'Menu1-2'
            },
            children: [{
                path: 'menu1-2-1',
                component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
                name: 'Menu1-2-1',
                meta: {
                  title: 'Menu1-2-1'
                }
              },
              {
                path: 'menu1-2-2',
                component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
                name: 'Menu1-2-2',
                meta: {
                  title: 'Menu1-2-2'
                }
              }
            ]
          },
          {
            path: 'menu1-3',
            component: () => import('@/views/nested/menu1/menu1-3'),
            name: 'Menu1-3',
            meta: {
              title: 'Menu1-3'
            }
          }
        ]
      },
      {
        path: 'menu2',
        component: () => import('@/views/nested/menu2/index'),
        name: 'Menu2',
        meta: {
          title: 'menu2',
          roles: ['editor']
        }
      }
    ]
  },
  // 如果需要配置重定向404页面的话,需要配置在asyncRoutes的最后
  {
    path: '*',
    redirect: '/404',
    hidden: true
  }
]
// 实例化vue的时候只挂载constantRouter
const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({
    y: 0
  }),
  routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}
export default router

2. src/permission.js动态添加路由

我们在登录成功后,router会重定向一个新页面,在跳转之前src/permission.js里面路由守卫router.beforeEach会先做一些拦截验证,根据判断会做不同页面跳转和操作,比如没登录就先跳到登录页,根据获取到的用户信息roles筛选路由后动态添加路由。
在这里插入图片描述
src/permisson.js具体实现代码与注释:

import router from './router'
import store from './store'
import {
  Message
} from 'element-ui'
// 页面进度条组件
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import {
  getToken
} from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({
  showSpinner: false
}) // NProgress 配置

const whiteList = ['/login', '/404'] // 不重定向的白名单

router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()

  // 设置页面标题
  document.title = getPageTitle(to.meta.title)

  // 确定用户是否已登录 
  const hasToken = getToken()

  // 判断是否存在token,没有就重新登陆
  if (hasToken) {
    if (to.path === '/login') {
      // 如果已登录,则重定向到主页
      next({
        path: '/'
      })
      NProgress.done()
    } else {
      // 确定用户是否通过getInfo获得了权限角色
      const hasRoles = store.getters.roles && store.getters.roles.length > 0 //这里指的是src/store/getters.js的roles
      // console.log(hasRoles)
      //判断是否已经有roles了
      if (hasRoles) {
        next(); //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
      } else {
        try {
          // get user info
          // 注意: roles 角色必须是对象数组! 例如: ['admin'] 或 ,['developer','editor']
          // 1. 获取roles
          const {
            roles
          } = await store.dispatch('user/getInfo') //第一步

          // 2. 根据角色生成可访问路由图
          // 获取通过权限验证的路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles) //第二步
          // 3. 更新加载路由
          router.options.routes = store.getters.permission_routes //第三步
          // 动态添加可访问路由
          router.addRoutes(accessRoutes)
          // console.log(store)
          // console.log(accessRoutes);

          // hack方法 确保addRoutes已完成,以确保地址是完整的
          // 设置replace: true,这样导航就不会留下历史记录
          next({
            ...to,
            replace: true
          })
        } catch (error) {
          // 删除token并转到登录页面重新登录
          await store.dispatch('user/resetToken')
          Message.error('出现错误~请重新登录')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* 没有token */

    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入

      next()
    } else {

      next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

3. store/modules/user.js

而获取+存储roles通过store/modules/user.js实现,筛选+存储路由又是通过通过store/modules/permission.js实现的
在这里插入图片描述
store/modules/user.js主要是获取和存储roles
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
store/modules/user.js完整代码:

import {
  login,
  logout,
  getInfo
} from '@/api/user'
import {
  getToken,
  setToken,
  removeToken
} from '@/utils/auth'
import {
  resetRouter
} from '@/router'

const getDefaultState = () => {
  return {
    token: getToken(),
    name: '',
    avatar: '',
    roles: [],
  }
}

const state = getDefaultState()

const mutations = {
  RESET_STATE: (state) => {
    Object.assign(state, getDefaultState())
  },
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  }
}

const actions = {
  // user login
  login({
    commit
  }, userInfo) {
    const {
      username,
      password
    } = userInfo
    return new Promise((resolve, reject) => {
      login({
        username: username.trim(),
        password: password
      }).then(response => {
        const {
          data
        } = response
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // get user info
  getInfo({
    commit,
    state
  }) {
    return new Promise((resolve, reject) => {
      // state.token之前没有传 出现了重复登陆问题
      getInfo(state.token).then(response => {
        const {
          data
        } = response
        if (!data) {
          return reject('验证失败,请重新登录')
        }
        const {
          name,
          roles,
          avatar
        } = data
        if (!roles || roles.length <= 0) {
          reject('getInfo:roles must be a non-null array!')
        }

        commit('SET_NAME', name)
        commit('SET_ROLES', roles)
        commit('SET_AVATAR', avatar)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({
    commit,
    state
  }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        removeToken() // must remove  token  first
        resetRouter()
        commit('RESET_STATE')
        commit('SET_ROLES', [])
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({
    commit
  }) {
    return new Promise(resolve => {
      removeToken() // must remove  token  first
      commit('RESET_STATE')
      resolve()
    })
  }
}

export default {
  // 加上这个会有报错,不加的话user/login这种方式用不了
  namespaced: true,
  state,
  mutations,
  actions
}

4. store/modules/permission.js筛选路由

store/modules/permission.js用于匹配权限,筛选角色对应的路由并存储起来

import {
  asyncRoutes,
  constantRoutes
} from '@/router'

/**
 * 使用 meta.role 以确定当前用户是否具有权限
 * @param roles
 * @param route
 */
// 匹配权限
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 * 通过递归过滤异步路由表
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = {
      ...route
    }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes) // 将过滤后的路由和constantRoutes存起来
  }
}

// 筛选
const actions = {
  generateRoutes({
    commit
  }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      // 管理员admin显示全部路由,
      // 我这里admin是想让它不显示全部的 想要admin能看见全部的话把注释去掉
      // if (roles.includes('admin')) {
      //   accessedRoutes = asyncRoutes || []
      // } else {
      //过滤路由
      accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      // accessedRoutes这个就是当前角色可见的动态路由
      // }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

这里的代码说白了就是干了一件事,通过用户的权限和之前在router.js里面asyncRoutes的每一个页面所需要的权限做匹配,最后返回一个该用户能够访问路由有哪些。

5. store/getter.js和store/index.js

当然了,新加的模块要记得引入进去
store/getter.js添加以下代码:
在这里插入图片描述

store/index.js
在这里插入图片描述

6. sidebar使用筛选后的路由

src\layout\components\Sidebar\index.vue
在这里插入图片描述
遍历之前算出来的permission_routers,通过vuex拿到之后动态v-for渲染

<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />

效果

如何看效果?如果是用的vue-admin-template的mock做登录和获取用户数据,登录用户名为admin则role为admin,用户名为editor则role为editor。
角色为admin看到的菜单栏
在这里插入图片描述

角色为editor看到的菜单栏
在这里插入图片描述

完整源码

记得给我一个star
https://gitee.com/yyy1203/vue-admin-template-permission.git

有兴趣可以看看花裤衩大佬文章:https://juejin.cn/post/6844903478880370701#heading-5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/40468.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java框架 Spring之 AOP 面向切面编程 切入点表达式 AOP通知类型 Spring事务

AOP(Aspect Oriented Programming)面向切面编程&#xff0c;一种编程范式&#xff0c;指导开发者如何组织程序结构 作用&#xff1a;在不惊动原始设计的基础上为其进行功能增强 Spring理念&#xff1a;无入侵式/无侵入式 我们在不修改源代码的时候&#xff0c;为了执行另外的…

idea如何快速找到项目中对应的类(包括源码)

文章目录1. 前言2. 先说结论3. idea的全局搜索功能 MethodValidation4. 搜索spring源码(例子)1. 前言 最近在看某些功能的时候&#xff0c;会去看对应的源码&#xff0c;而有时候只知道类名&#xff0c;不知道从哪里进入源码&#xff0c;因此就比较好奇&#xff0c;idea的全局…

JAVA类加载器

JAVA是一种解释型语言&#xff0c;也就是一种边解释边执行的语言。JAVA所有源代码在执行之前&#xff0c;先要被编译成class文件&#xff0c;然后类加载器加载解析class文件&#xff0c;最后才执行。 JVM自带了几个类型的类加载器&#xff0c;JVM使用分层的软件架构方式设计类…

蜂鸟E203学习笔记(二)--蜂鸟E203总体框架

蜂鸟E203总体框架 蜂鸟E203处理器系统如下图所示 一、蜂鸟E203处理器核设计总览和顶层 1.1 蜂鸟E203处理器核的设计理念 模块化和可重用性&#xff1a;将处理器分成几个主体模块&#xff0c;每个单元之间的接口简单清晰。面积最小化&#xff1a;追求低功耗和小面积&#x…

DS18B20

一、DS18B20初始化时序图 &#xff08;1&#xff09; 先将数据线置高电平“1”。 &#xff08;2&#xff09; 延时&#xff08;该时间要求的不是很严格&#xff0c;但是尽可能的短一点&#xff09;。 &#xff08;3&#xff09; 数据线拉到低电平“0”。 &#xff08;4&#xf…

【配送路径规划】基于matlab遗传算法求解静态外卖骑手路径规划问题【含Matlab源码 2248期】

⛄一、遗传算法求解静态外卖骑手路径规划问题 1 模型假设 外卖配送的实际运行是一个复杂的过程, 受诸多因素影响, 为了建立调度模型, 本文做如下假设。 (1) 外卖配送更多的是服务特殊群体, 所以本文认为外卖配送是一种预约型配送, 即在进行调度安排前, 己经获取了所有顾客的地…

Java并发-生产者消费者实现

生产者与消费者模型介绍 定义&#xff1a; 生产者消费者模式是一个十分经典的多线程并发协作的模式。 意义&#xff1a;弄懂生产者消费者问题能够让我们对并发编程的理解加深。 介绍&#xff1a;所谓生产者 - 消费者问题&#xff0c;实际上主要是包含了两类线程&#xff0c;…

Python面向对象编程之对象行为与特殊方法

面向对象编程之对象行为与特殊方法 python中的对象通常根据它们的行为和实现的功能进行分类。例如&#xff0c;所有序列类型都分在一组&#xff0c;如字符串&#xff0c;列表和元组&#xff0c;就是因为它们都支持一组相同的序列操作&#xff0c;如s[n], len[s]等。 所有基本…

【学习总结】LSD-SLAM配置与运行记录

今天安装测试了LSD-SLAM&#xff0c;记录配置中遇到的问题。 LSD-SLAM论文 LSD-SLAM: Large-Scale Direct Monocular SLAM, J. Engel, T. Schps, D. Cremers, ECCV 14 Semi-Dense Visual Odometry for a Monocular Camera, J. Engel, J. Sturm, D. Cremers, ICCV 13 配置环境…

SpringBoot实现多数据源(四)【集成多个 Mybatis 框架】

上一篇文章《SpringBoot实现多数据源&#xff08;三&#xff09;【AOP 自定义注解】》 四、集成多个 Mybatis 框架 实现步骤 创建一个 dynamic_mybatis 的springboot项目&#xff0c;导入依赖 pom.xml <dependencies><!--jdbc--><dependency><groupId…

Fiddler抓取手机app包

文章目录1. 配置fiddler1.1 下载fiddler1.2 配置fiddler2. 安装证书2.1 查询主机ip2.2下载证书2.3 手机安装证书2.4 查询安装的证书3. 手机设置代理4. 测试是否抓包成功大前提&#xff1a;手机和Fiddler所在的主机在同一网段&#xff0c;且能够互相访问 1. 配置fiddler 1.1 下…

第三十八篇 Vue中封装Swiper组件 2.0

上一篇内容讲到封装Swiper组件的一个过程&#xff0c;如果是静态的数据封装组件初始化在mounted当中并无多大影响&#xff0c;但是这样封装的组件复用性较低或者可能只使用一次&#xff0c;那么在动态使用通过ajax请求数据需要面临的是swiper初始化过早的问题&#xff0c;在mou…

【车间调度】基于matlab混合蛙跳算法 (SFLA) 求解简单调度问题【含Matlab源码 2247期】

⛄一、车间调度简介 在传统的SFLA中&#xff0c;每一个青蛙的位置代表一个解&#xff0c;若干个青蛙组成的种群代表一个解的集合&#xff0c;种群被划分为不同的组&#xff0c;即模因组&#xff0c;对每个模因组执行搜索过程&#xff0c;当达到终止条件后&#xff0c;重新将模…

编译原理13:SLR(1)分析表、LR(1)分析表

更强的LR分析 可以根据当前单词&#xff0c;来选择是移进还是归约。只要所有移进项目中的点后面的那些终结符&#xff0c;与归约项目生成的非终结符的Follow集合的元素没有重叠。若当前单词属于上述Follow集合里则规约 SLR(1)冲突解决办法 SLR(1)分析表的构造 SLR(1)分析表的构…

001. 组合

1.题目链接&#xff1a; 77. 组合 2.大概思路&#xff1a; 2.1题目要求&#xff1a; 给两个值 n 和 k &#xff0c;要求从[1&#xff0c;n]的区间中&#xff0c;输出所有元素数量为k的组合。&#xff08;不能有[1,1]&#xff0c;值只能取一次&#xff09; 2.2思路&#xff…

(十七)Spring6整合JUnit

文章目录环境Spring对JUnit4的支持Spring对JUnit5的支持上一篇&#xff1a;&#xff08;十六&#xff09;Spring对事务的支持 环境 spring6里程碑版本的仓库 依赖&#xff1a;spring context依赖、spring对junit的支持相关依赖、junit4依赖、junit5依赖 <!--配置多个仓库-…

Spring【Spring的创建与使用】

Spring【Spring的创建项目与使用】&#x1f34e;一.Spring创建项目&#x1f352;1.1 创建⼀个 Maven 项⽬&#x1f352;1.2 添加 Spring 框架⽀持&#x1f352;1.3 添加启动类&#x1f34e;二.Bean对象的存储与获取&#x1f352;2.1 存储 Bean 对象&#x1f349; 2.1.1 创建 Be…

C语言【微项目19】—大整数字符串乘法器[纯字符串乘法][乘法表与加法表]【2022-11-27】

C语言【微项目19】—大整数字符串乘法器[纯字符串乘法][乘法表与加法表]【2022-11-27】1.函数功能2 简要测试结果3.BigInterNoLimitMutiString.c3.大整数字符串乘法器实现思路4. 大整数字符串乘法器典型使用流程main.c【TDTX】 【C99】 【编译与运行环境】64位Windows操作系统&…

Unity嵌入Android项目开发

目录前言1 搭建开发环境2 创建Unity项目2.1 新建项目2.2 Unity构建配置2.3 Android环境相关配置2.4 导出Unity库文件3 创建Android项目3.1 新建Android项目3.2 Android环境相关配置3.2 导入Unity相关的库3.3 Android中跳转到Unity视图4 进阶扩展4.1 包体积优化4.1.1 mono和IL2C…

ILRuntime1.安装

目录 1&#xff1a;官网地址&#xff1a;介绍 — ILRuntime 2&#xff1a;注意事项&#xff1a; 3&#xff1a;安装 此文章是参照官方文档&#xff0c;自己的开发笔记。 1&#xff1a;官网地址&#xff1a;介绍 — ILRuntime 官方Unity示例代码&#xff1a;GitHub - Ourpa…