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. 尝试运行

项目结构:

相关推荐
Mr_Mao4 小时前
Naive Ultra:中后台 Naive UI 增强组件库
前端
前端小趴菜055 小时前
React-React.memo-props比较机制
前端·javascript·react.js
摸鱼仙人~6 小时前
styled-components:现代React样式解决方案
前端·react.js·前端框架
sasaraku.7 小时前
serviceWorker缓存资源
前端
RadiumAg8 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo8 小时前
ES6笔记2
开发语言·前端·javascript
yanlele8 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子9 小时前
React状态管理最佳实践
前端
烛阴10 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子10 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端