Vue3 权限控制:利用动态路由与自定义指令

前言

在现代前端开发中,权限控制是一个至关重要的环节。无论是后台管理系统还是企业级应用,都需要根据用户角色或权限级别来限制界面元素的可见性和可访问性。

在前后端分离的架构中,前端不再拥有完整的权限判断能力,必须依赖后端提供的权限信息。

注意,以下实现之后,每次使用权限相关功能还需要后端校验保证安全。

实现

页面权限

页面的访问,是根据配置路由表的路径来访问页面。那么,我们控制了路由表,就能控制页面的访问。

实现思路:登录 -> 获取权限信息 -> 动态添加可访问路由。

传统的静态路由配置无法满足现代应用的权限需求,需在前端使用动态路由实时修改,无需刷新整个应用。

基本页面

bash 复制代码
src
 └─views
    ├─home.vue
    ├─menu.vue
    └─user.vue
html 复制代码
<!-- home.vue -->
<template>
  <div>主页</div>
</template>

<!-- menu.vue -->
<template>
  <div>菜单管理</div>
</template>

<!-- user.vue -->
<template>
  <div>用户管理</div>
</template>

配置路由

bash 复制代码
src
 └─router
    ├─modules
    │  ├─remaining.js
    │  └─dynamic.js
    └─index.js
  • remaining 存放公共路由,没有权限页可以访问,比如登录页、404页面这些

    ts 复制代码
    // remaining.js
    import Layout from "@/layout/index.vue";
    const remainingRouter = [
      {
        path: "/remaining",
        component: Layout,
        redirect: "/remaining/home",
        meta: { title: "公共页面", roles: ["admin", "user"] },
        children: [
          {
            path: "/remaining/home",
            component: () => import("@/views/home.vue"),
            name: "Home",
            meta: { title: "首页", icon: "home" },
          },
        ],
      },
      {
        path: "/:pathMatch(.*)*",
        name: "404",
        component: () => import("@/views/404.vue"),
        meta: { hidden: true },
      },
    ];
    export default remainingRouter;
  • dynamic动态路由模块,需要权限访问。

    ts 复制代码
    // router/modules/dynamic.js
    import Layout from '@/layout/index.vue'
    
    // 动态路由(需要权限)
    export const dynamicRouter = [
      {
        path: '/system',
        component: Layout,
        redirect: '/system/user',
        meta: { title: '系统管理', icon: 'system', roles: ['admin'] },
        children: [
          {
            path: 'user',
            component: () => import('@/views/user.vue'),
            name: 'User',
            meta: { title: '用户管理', icon: 'user' }
          },
          {
            path: 'menu',
            component: () => import('@/views/menu.vue'),
            name: 'Menu',
            meta: { title: '菜单管理', icon: 'menu' }
          }
        ]
      }
    ]
    
    export default dynamicRouter
  • index.ts 路由配置

    ts 复制代码
    import { createRouter, createWebHistory } from "vue-router";
    import { useAuthStore, usePermissionStore } from "@/stores";
    import remainingRouter from "./modules/remaining";
    
    export const constantRoutes = [...remainingRouter];
    
    // 基础配置
    const router = createRouter({
      history: createWebHistory(),
      routes: constantRoutes,
      scrollBehavior: () => ({ left: 0, top: 0 }),
    });
    
    // 白名单路由(无需登录)
    const WHITE_LIST = ["Home", "404"];
    
    // 全局前置守卫
    router.beforeEach(async (to, from) => {
      const authStore = useAuthStore();
    
      // 已登录
      if (authStore.token) {
        return true;
      }
      // 未登录
      else {
        // 白名单路由直接放行
        if (WHITE_LIST.includes(to.name)) {
          return true;
        }
        return { path: "/remaining/home" };
      }
    });
    
    export default router;

Pinia 储存登录状态

bash 复制代码
src
 └─stores
    ├─modules
    │  ├─auth.store.js
    │  └─permission.store.js
    └─index.js
  • auth.store.js 控制登录退出

    js 复制代码
    import { defineStore } from "pinia";
    import router from "@/router";
    import { usePermissionStore } from "@/stores";
    
    export const useAuthStore = defineStore("auth", {
      state: () => ({
        token: localStorage.getItem("token") || null,
        roles: [],
      }),
    
      actions: {
        // 用户登录
        async login() {
          this.token = "12345";
          this.roles = ["admin"];
          localStorage.setItem("token", this.token);
    
          // 添加动态路由
          const permissionStore = usePermissionStore();
          const accessRoutes = permissionStore.generateRoutes(this.roles);
          accessRoutes.forEach((route) => {
            router.addRoute(route);
          });
        },
    
        // 用户登出
        logout() {
          this.token = null;
          this.roles = [];
          localStorage.removeItem("token");
          router.replace({ name: "Home" });
        },
      },
    });
  • permission.store.js 控制动态添加路由

    js 复制代码
    import { defineStore } from "pinia";
    import dynamicRoutes from "@/router/modules/dynamic";
    import { constantRoutes } from "@/router";
    
    export const usePermissionStore = defineStore("permission", {
      state: () => ({
        routes: [],
        addRoutes: [],
      }),
    
      actions: {
        // 生成可访问路由
        generateRoutes(roles) {
          let accessedRoutes;
    
          if (roles.includes("admin")) {
            accessedRoutes = dynamicRoutes; // 管理员获取所有路由
          } else {
            accessedRoutes = [];
          }
    
          this.addRoutes = accessedRoutes;
          this.routes = constantRoutes.concat(accessedRoutes);
          return accessedRoutes; // 返回生成的路由
        },
      },
    });
  • index.js 导出 pinia 实例

    js 复制代码
    import { createPinia } from 'pinia'
    const pinia = createPinia()
    
    export * from './modules/auth.store.js'
    export * from './modules/permission.store.js'
    export default pinia

这样即可实现动态添加路由

按钮权限

按钮权限,通过访问权限是否存在而决定是否显示按钮。

可以通过全局挂载自定义指令访问 pinia 中的权限,如果存在对应权限则显示按钮

配置 Directive

src 复制代码
src
 └─directive
    └─permission.js
  • permission.js 自定义指令

    js 复制代码
    import { watchEffect } from "vue";
    import { useAuthStore } from "@/stores";
    
    const permissionDirective = {
      mounted(el, binding) {
        const checkPermission = () => {
          const authStore = useAuthStore();
          const hasPermission = binding.value && binding.value.some((permission) =>
            authStore.roles.includes(permission)
          );
    
          el.style.display = hasPermission ? "" : "none";
        };
    
        // 初始检查 + 响应式监听权限变化
        checkPermission();
        watchEffect(checkPermission);
      },
    };
    
    export default permissionDirective;

之后挂载到 app 即可实现全局按钮权限控制。

全局配置

简单实现一个布局

bash 复制代码
src
 └─layout
    └─index.vue
  • index.vue

    html 复制代码
    <template>
        <div>
            <button @click="$router.push({ name: 'Home' })" >Home</button>
            <button @click="$router.push({ name: 'User' })" v-permission="['admin']">User</button>
            <button @click="$router.push({ name: 'Menu' })" v-permission="['admin']">Menu</button>
            <button @click="authStore.login()">登录</button>
            <button @click="authStore.logout()">登出</button>
            <RouterView />
        </div>
    </template>
    
    <script setup>
    import { RouterView } from 'vue-router'
    import { useAuthStore } from "@/stores";
    const authStore = useAuthStore();
    </script>

将之前的内容挂载到 main

  • main.ts 加载

    ts 复制代码
    import { createApp } from 'vue'
    import router from './router'
    import pinia from './stores'
    import App from './App.vue'
    import permissionDirective from './directive/permission'
    
    const app = createApp(App)
    app.directive('permission', permissionDirective); // 注册为 v-permission
    app.use(pinia)
    app.use(router)
    app.mount('#app')

运行

  1. 运行项目:npm run dev
  2. 进入访问页:localhost:5173/remaining
  3. 尝试运行

项目结构:

相关推荐
派小汤6 分钟前
Springboot + Vue + WebSocket + Notification实现消息推送功能
vue.js·spring boot·websocket
郁大锤18 分钟前
Flask与 FastAPI 对比:哪个更适合你的 Web 开发?
前端·flask·fastapi
HelloRevit1 小时前
React DndKit 实现类似slack 类别、频道拖动调整位置功能
前端·javascript·react.js
阿珊和她的猫1 小时前
Webpack Dev Server的安装与配置:解决跨域问题
vue.js·webpack
ohMyGod_1232 小时前
用React实现一个秒杀倒计时组件
前端·javascript·react.js
eternal__day2 小时前
第三期:深入理解 Spring Web MVC [特殊字符](数据传参+ 特殊字符处理 + 编码问题解析)
java·前端·spring·java-ee·mvc
醋醋2 小时前
Vue2源码记录
前端·vue.js
艾克马斯奎普特2 小时前
Vue.js 3 渐进式实现之响应式系统——第四节:封装 track 和 trigger 函数
javascript·vue.js
敲代码的玉米C2 小时前
Vue Draggable 深入教程:从配置到实现的完整指南
vue.js
frontDeveloper2 小时前
Vue3基础使用概览
vue.js