vue 动态路由权限控制实现方案详解篇

文章前言

在基础的 vue 项目中都是默认以前端配置好静态路由

但在一些较为成熟或大型项目中我们会通常会遇到 权限控制 这个问题,这样我们就涉及到动态路由的设置了

特别是一些 后台管理系统项目,需要对不同的用户进行角色区分,对不同的角色的操作权限进行限制

如果单纯按照前端配置好的静态路由,就会出现用户登陆后,即使没看到页面的入口,也可以通过输入游览器地址url进入被禁止的页面,因为在项目运行时,路由边已经生成了,所以我们需要对需要权限控制的路由进行动态的配置生成

有2两种的方法对路由进行动态设置

1、简单角色路由:根据服务器返回的用户角色权限,前端本地加载不同的路由配置生成路由

2、复杂权限路由:角色页面权限由服务器控制,服务器返回路由页面数据信息,前端根据路由信息生成路由

简单角色动态路由

首先后台系统一般都有一个基础 layout 布局入口页面,再从该页面中去衍生子菜单路由,展示页面入口,本篇将首页配置为布局layout页面,再从主页面中展示具体的子路由页面

1、配置路由文件,定义路由信息

将不需要权限的路由信息写入 routes 数组并生成,例如登录login

将动态配置的权限路由写到 asyncRoutes 数组

/src/router/index.js

js 复制代码
import Vue from "vue";
import VueRouter from "vue-router";
import store from "@/store";
Vue.use(VueRouter);
// 默认静态路由
const routes = [
  {
    path: "/login",
    name: "login",
    component: () => import("@/views/login/index.vue"),
  },
];
// 动态配置的路由(配置权限)
export const asyncRoutes = [
  {
    path: "/test1",
    name: "Test1",
    component: () => import("@/views/test1/index.vue"),
    meta: {
      roles: ["admin", "editor"], //  拥有这些角色权限的用户才能访问
      title: "测试1",
    },
    children: [
      {
        path: "children1",
        name: "Test1Children1",
        component: () => import("@/views/test1/test1-children1/index.vue"),
        meta: { roles: ["admin"], title: "测试1子路由1" },
      },
      {
        path: "children2",
        name: "Test1Children2",
        component: () => import("@/views/test1/test1-children2/index.vue"),
        meta: { roles: ["editor"], title: "测试1子路由2" },
      },
      {
        path: "children3",
        name: "Test1Children3",
        component: () => import("@/views/test1/test1-children3/index.vue"),
        meta: { roles: ["admin"], title: "测试1子路由3" },
      },
    ],
  },
  {
    path: "/test2",
    name: "Test2",
    component: () => import("@/views/test2/index.vue"),
    meta: { roles: ["admin"],  title: "测试2" },
    children: [],
  },
];
// 创建一个路由器实例
const createRouter = () =>
  new VueRouter({
    mode: 'hash',
    routes,
  });
// 创建路由器
const router = createRouter();
// 重置路由器
export function resetRouter() {
  const newRouter = createRouter()
  // 重置router对象
  router.matcher = newRouter.matcher;
  // 提交用户路由的清除状态的mutation
  store.commit("user/CLEAR_ROUTERS");
}
export default router;

2、permission模块,集成封装根据权限过滤路由的vuex方法

创建vuex模块 permission.js

写入generateRoutes方法,方便项目中全局调用,传入角色权限数组来进行路由过滤

根据 权限角色过滤动态配置路由 得到需要生成的路由信息

/src/store/modules/permission.js

js 复制代码
import { asyncRoutes } from "@/router";
const state = {
  routes: [], // 动态配置的路由
};
const mutations = {
  SET_ROUTES: (state, routes) => {
    state.routes = routes;
  },
};
const actions = {
  /**
   * 生成路由
   * @param {Object} commit - 提交方法
   * @param {Array} roles - 用户角色权限数组
   * @returns {Promise} - 返回一个Promise对象,用于异步处理
   */
  generateRoutes({ commit }, roles) {
    return new Promise((resolve) => {
      let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); // 传入动态配置路由数组,角色权限数组
      console.log(accessedRoutes, 'accessedRoutes')
      commit("SET_ROUTES", accessedRoutes);
      resolve(accessedRoutes);
    });
  },
};
export default {
  namespaced: true,
  state,
  mutations,
  actions,
};

在action异步 generateRoutes 方法中调用 filterAsyncRoutes 方法对 asyncRoutes 数组进行过滤

filterAsyncRoutes 方法使用递归进行过滤直到没有children字段为止

js 复制代码
/**
 * 使用 meta.role 确定当前用户是否具有权限
 * @param {Array} roles - 用户的角色列表
 * @param {Object} route - 路由对象
 * @returns {boolean} - 如果用户具有访问权限则返回true,否则返回false
 */
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 => {
    // 创建临时变量,复制route对象
    const tmp = { ...route }
    // 判断是否有权限
    if (hasPermission(roles, tmp)) {
      // 如果有权限,继续遍历子路由
      if (tmp.children) {
        // 递归调用filterAsyncRoutes函数,过滤子路由
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      // 将符合条件的路由添加到结果数组中
      res.push(tmp)
    }
  })
  // 返回结果数组
  return res
}

3、user模块,模拟获取用户信息方法

创建vuex模块 user.js ,用于模拟接口获取用户角色信息并保存状态

/src/store/modules/user.js

js 复制代码
const state = {
  userInfo: {}, // 用户信息
  roles: [], // 角色权限数组
};
const mutations = {
  SET_USERINFO(state, userInfo) {
    state.userInfo = userInfo;
  },
  SET_ROLES(state, roles) {
    state.roles = roles;
  },
};
const actions = {
  // 获取用户信息方法
  getInfo({ commit }) {
    return new Promise((resolve) => {
      const data = {
        name: "admin",
        avatar: "",
        roles: ["admin"],
        introduction: "I am a super administrator",
        status: 1,
        email: "",
        phone: "1234567890",
        id: 1,
        createTime: "2016-11-22 10:30:30",
        lastLoginTime: "2019-05-27 10:30:30",
        lastLoginIp: "127.0.0.1",
      };
      commit("SET_USERINFO", data); // 设置用户信息
      commit("SET_ROLES", data.roles); // 设置角色权限
      // 延时模拟异步
      setTimeout(() => {
        resolve(data);
      }, 500);
    });
  },
};
export default {
  namespaced: true,
  state,
  mutations,
  actions,
};

返回['admin'] 角色权限时 ,过滤后的路由输出如下,返回包含admin权限的路由

4、创建全局路由守卫,控制路由权限并动态生成路由

定义好了上述的一系列方法,那么在什么时机调用,并且添加到路由中呢?

1.正确的时机应当是 用户已经登录并取得得用户信息后,再根据用户角色过滤路由,最后生成路由

2.当用户退出登录或者未登录时,清空用户信息以及路由,恢复为初始状态,跳转回登录页

使用 vue-router的全局路由守卫 定义一个路由权限文件 进行权限控制

如下代码为假设已经登录获取token或sessionId用户认证的情况下

/src/router/permission.js

js 复制代码
import router, { resetRouter } from "@/router";
import store from "@/store";

const whiteList = ["/login"]; // 路由白名单,无需登录便可以访问的路由

router.beforeResolve(async (to, from, next) => {
  // 获取用户登录的token
  const hasToken = "xxxxxxxx"; // getToken() // 这里用虚拟值代替,表示已登录

  // 判断当前用户是否登录
  if (hasToken) {
    if (to.path === "/login") {
      // 如果当前用户已经登录,则跳转到首页
      next({ path: "/" });
    } else {
      // 从store user模块中获取用户权限角色,如果有角色则代表已经获取了用户信息登录中,直接放行
      const hasRoles = store.state.user.roles && store.state.user.roles.length > 0;
      if (hasRoles) {
        next();
      } else {
        // 获取了token,但是没有角色,则调用获取用户信息接口获取角色权限数组
        try {
          // 调用获取存储用户信息,并取得角色权限数组,例如 ['admin'] or ,['developer','editor']
           const { roles } = await store.dispatch("user/getInfo");
          // 获取用户角色权限数组后调用vuex的generateRoutes方法过滤掉没有权限的路由表并返回
          const accessRoutes = await store.dispatch("permission/generateRoutes", roles);
          // 使用addRoute将 动态配置的路由 添加到 layout 首页子级中
          router.addRoute({
            path: "/",
            name: "home",
            redirect: accessRoutes[0].path,
            component: () => import("@/views/layout"),
            meta: { title: "首页" }, // 路由元信息
            children: accessRoutes,
          });
          // 然后再添加404导航路由,防止刷新后默认导向404页
          router.addRoute({
            path: "/404",
            redirect: "/404",
            hidden: true,
            component: () => import("@/views/notFoundPage.vue"),
          });
          router.addRoute({ path: "*", redirect: "/404", hidden: true });
          // 设置replace:true,这样导航就不会留下历史记录
          next({ ...to, replace: true });
        } catch (error) {
          // 捕获错误异常退出登录清除用户信息
          resetRouter(); // 重置路由
          // await store.dispatch("user/resetToken");
          next(`/login?redirect=${to.path}`);
        }
      }
    }
  } else {
    // 用户未登录
    resetRouter(); // 重置路由
    if (whiteList.indexOf(to.path) !== -1) {
      // 需要跳转的路由是否是白名单whiteList中的路由,若是,则直接跳转
      next();
    } else {
      // 需要跳转的路由不是白名单whiteList中的路由,直接跳转到登录页
      next(`/login?redirect=${to.path}`);
    }
  }
});

最终效果如下:

核心小结

核心流程为以下的步骤

1、在路由全局前置守卫中,首先获取token或者sessionId等身份认证信息,表示已登录,未登录并且不在白名单内的路由统一跳转回登录页

2、在已登录环境作用域中 判断是否已经取得用户信息,例如本篇需要用到的用户权限角色,如果存在角色权限信息则代表已经处于登录后生成了动态路由配置。如果没有,则进行下一步

3、从vuex 获取用户权限角色后,再调用 过滤路由的方法,返回过滤后的路由信息调用addRoute()添加路由,并且为防止页面刷新时会重置路由后找不到对应的路由地址会导向404页,所以将404页放在 等待接口返回用户信息获取完成,动态添加路由后 再添加

复杂权限路由

当系统角色比较多,页面路由较多较为复杂时,如果继续让前端在本地配置路由的方式就会显得比较臃肿麻烦

所以衍生出另外一种方式,由后端数据库返回路由信息,前端根据数据动态添加路由,统一动态配置

这样每当角色更改,角色可访问路由发生变化时,只需对后台数据进行修改,无需前端进行系统更新,省去了一波麻烦事

具体实现逻辑和上述讲的大同小异,区别在于获取路由信息的不同

1、模拟返回mock路由信息数据

首先定义一组模拟接口返回的mock路由数据,用在获取用户信息接口中

/src/utils/routes.js

js 复制代码
export default [
  {
    path: "/test1",
    name: "Test1",
    meta: { title: "测试1" },
    children: [
      {
        path: "test1-children1",
        name: "Test1Children1",
        meta: { title: "测试1子路由2" },
      },
      {
        path: "test1-children2",
        name: "Test1Children2",
        meta: { title: "测试1子路由2" },
      },
    ],
  },
  {
    path: "/test2",
    name: "Test2",
    children: [],
    meta: { title: "测试2" },
  }
];

在原有文件,上面例子vuex的user模块文件中,在getInfo()获取用户信息方法中模拟返回

/src/store/modules/user.js

js 复制代码
import routes from '@/utils/routes.js'

const actions = {
  // 获取用户信息
  getInfo({ commit }) {
    return new Promise((resolve) => {
      const data = {
        name: "admin",
        avatar: "",
        roles: ["admin"],
        introduction: "I am a super administrator",
        status: 1,
        email: "",
        phone: "1234567890",
        id: 1,
        createTime: "2016-11-22 10:30:30",
        lastLoginTime: "2019-05-27 10:30:30",
        lastLoginIp: "127.0.0.1",
        routes, // 路由信息数组
      };
      commit("SET_USERINFO", data); // 设置用户信息
      commit("SET_ROLES", data.roles); // 设置角色权限
      // 延时模拟异步
      setTimeout(() => {
        resolve(data);
      }, 500);
    });
  },
};

由后端接口返回路由信息数组后,前端就不用自己根据角色进行过滤,而是根据数组数据匹配本地目录生成路由

2、全局路由守卫匹配文件目录并添加路由

在原有文件,上面例子全局路由守卫中进行修改

/src/router/permission.js

js 复制代码
// 调用获取存储用户信息,并取得可访问的路由数组
const { routes } = await store.dispatch("user/getInfo");
const routers = generateRouters(routes); // 匹配本地文件目录路由
store.commit("permission/SET_ROUTES", routers); // 将路由表存入vuex

// 使用addRoute将 动态配置的路由 添加到 layout 首页子级中
router.addRoute({
  path: "/",
  name: "home",
  redirect: routers[0].path,
  component: () => import("@/views/layout"),
  meta: { title: "首页" }, // 路由元信息
  children: routers, // 匹配后的路由数组
});

定义 generateRouters方法,使用递归,对接口返回的路由数组进行文件目录匹配

js 复制代码
/**
 * 匹配本地文件目录路由
 * @param {Array} routes - 路由数组
 * @param {string} parentPath - 父路径
 * @param {number} level - 层级,默认为0
 * @returns {Array} - 生成的路由数组
 */
function generateRouters(routes, parentPath = '', level = 0) {
  routes.forEach((item) => {
    let path // 匹配文件路径
    if (level === 0) { // 第一层路由
      path = item.path
      item.component = () => import(`@/views${path}/index.vue`);
    } else { // 子路由
      path = parentPath + `/${item.path}`
      item.component = () => import(`@/views${path}/index.vue`);
    }
    if (item.children && item.children.length > 0) {
      item.children = generateRouters(item.children, path, level + 1);
    }
  })
  return routes
}

layout 主页面处理

最后,处理layout主页面如何显示

本篇先放出实现的代码,做一下简洁预览,实际业务要根据后端的数据结构进行更改

使用element ui,递归子菜单item组件来进行展示

首页代码

/src/views/layout/index.vue

html 复制代码
<template>
  <el-row class="tac">
    <el-col :span="4">
      <el-menu
        :default-active="defaultActive"
        class="el-menu-vertical-demo"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
      >
        <SidebarItem v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-col>
    <el-col :span="20">
      <router-view></router-view>
    </el-col>
  </el-row>
</template>
<script>
import SidebarItem from './components/SidebarItem.vue'
export default {
  components: {
    SidebarItem,
  },
  data() {
    return {
      defaultActive: "/dashboard",
    };
  },
  watch: {
    // 监听路由变化
    $route(route) {
      this.defaultActive = route.name;
    }
  },
  computed: {
    permission_routes() {
      return this.$store.state.permission.routes // 动态配置的路由
    },
  },
  created() {
    this.defaultActive = this.$route.name; // 初始化已选中菜单
  },
};
</script>

菜单子项item组件代码

菜单子项item处理的组件

/src/view/layout/components/SidebarItem.vue

html 复制代码
<template>
  <div v-if="!item.hidden">
    <template v-if="!item.children || item.children.length === 0" >
      <el-menu-item :index="item.name" @click="toggleMenu(item)">
        <span slot="title">{{ item.meta.title }}</span>
      </el-menu-item>
    </template>

    <el-submenu v-else :index="item.path">
      <template slot="title">{{ item.meta.title }}</template>
      <SidebarItem
        v-for="child in item.children"
        :key="child.path"
        :item="child"
        :base-path="child.path"
      />
    </el-submenu>
  </div>
</template>
<script>
export default {
  name: "SidebarItem",
  props: {
    // 当前 route 对象
    item: {
      type: Object,
      required: true,
    },
    // 当前 route 路径
    basePath: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      routeName: ""
    }
  },
  watch: {
    // 监听路由变化
    $route(route) {
      this.routeName = route.name;
    }
  },
  created() {
    this.routeName = this.$route.name;
  },
  methods: {
    toggleMenu(item) {
      // 当前路由与菜单项路由的name相等时,不做任何处理
      if (this.routeName === item.name) {
        return
      }
      // 否则,执行路由跳转
      this.$router.push({
        name: item.name,
      });
    },
  },
};
</script>
相关推荐
2301_7665360528 分钟前
调试无痛入手
开发语言·前端
@大迁世界2 小时前
构建 Next.js 应用时的安全保障与风险防范措施
开发语言·前端·javascript·安全·ecmascript
IT、木易3 小时前
ES6 新特性,优势和用法?
前端·ecmascript·es6
计算机软件程序设计3 小时前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序
指尖时光.3 小时前
【前端进阶】01 重识HTML,掌握页面基本结构和加载过程
前端·html
前端御书房3 小时前
Pinia 3.0 正式发布:全面拥抱 Vue 3 生态,升级指南与实战教程
前端·javascript·vue.js
NoneCoder3 小时前
JavaScript系列(84)--前端工程化概述
前端·javascript·状态模式
晚安7203 小时前
idea添加web工程
java·前端·intellij-idea
一个 00 后的码农4 小时前
25轻化工程研究生复试面试问题汇总 轻化工程专业知识问题很全! 轻化工程复试全流程攻略 轻化工程考研复试真题汇总
面试·面试问题·25考研·考研复试·考研调剂·面试真题·轻化工程
刘小炮吖i4 小时前
Java基础常见的面试题(易错!!)
java·面试·职场和发展