Vue Router:如何构建一个基于Vue 3的动态权限路由系统?

Vue Router:构建基于Vue 3的动态权限路由系统

大家好,今天我们来深入探讨如何利用 Vue Router 在 Vue 3 项目中构建一个动态权限路由系统。权限控制是现代 Web 应用中必不可少的一部分,它确保只有授权用户才能访问特定的页面和功能。我们将从理论到实践,一步步构建一个健壮且可维护的权限路由系统。

1. 权限路由系统的核心概念

在开始之前,我们先明确几个核心概念:

  • 路由(Route): 指的是应用中一个 URL 地址及其对应的组件。
  • 权限(Permission/Role): 代表用户在系统中拥有的访问资源的权利。可以是具体的权限标识符,也可以是角色名称。
  • 动态路由(Dynamic Route): 指的是在应用运行时根据用户的权限动态添加的路由。
  • 路由守卫(Navigation Guard): Vue Router 提供的钩子函数,用于在路由导航过程中进行权限验证和其他操作。

权限路由系统的核心目标是:根据用户的权限,动态地生成路由表,并使用路由守卫来控制用户对不同页面的访问。

2. 系统架构设计

一个典型的动态权限路由系统包含以下几个关键组成部分:

  • 用户认证模块(Authentication Module): 负责用户的登录、注销和权限信息的获取。
  • 权限管理模块(Permission Management Module): 负责权限信息的存储、管理和更新。
  • 路由生成模块(Route Generation Module): 负责根据用户的权限信息生成可访问的路由表。
  • 路由守卫模块(Navigation Guard Module): 负责在路由导航过程中进行权限验证,并根据验证结果进行页面跳转。

3. 技术选型

  • Vue 3: 作为前端框架,提供组件化开发和响应式数据绑定。
  • Vue Router: 作为路由管理器,负责路由的定义、导航和守卫。
  • Vuex/Pinia: 作为状态管理工具,用于存储用户认证信息和权限信息。如果项目规模较小,也可以考虑使用 reactiveref 来管理全局状态。
  • Axios/Fetch: 作为 HTTP 客户端,用于与后端 API 进行交互。

4. 代码实现:逐步构建动态权限路由系统

我们将按照以下步骤来构建我们的动态权限路由系统:

4.1. 初始化 Vue 项目

首先,使用 Vue CLI 创建一个新的 Vue 3 项目:

vue create dynamic-permission-router

在创建过程中,选择 Vue 3 预设,并根据需要选择其他功能,例如 Vue Router 和 Vuex/Pinia。

4.2. 安装依赖

安装必要的依赖:

npm install vue-router@next axios
# 如果选择 Vuex
npm install vuex@next
# 如果选择 Pinia
npm install pinia

4.3. 创建路由实例

src/router/index.js 文件中,创建 Vue Router 实例:

import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import Login from '../views/Login.vue';

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: {
      requiresAuth: false // 不需要登录
    }
  },
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: {
      requiresAuth: true // 需要登录
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
    meta: {
      requiresAuth: true // 需要登录
    }
  },
  // 404 Not Found 页面
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue'),
    meta: {
      requiresAuth: false
    }
  }
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

export default router;

这里我们定义了几个基本的路由,包括登录、首页和关于页面。每个路由都包含一个 meta 字段,用于存储路由的元数据,例如 requiresAuth 表示该路由是否需要登录才能访问。

4.4. 用户认证模块

创建一个 src/store/index.js 文件 (如果使用 Vuex) 或者 src/stores/index.js (如果使用 Pinia) 来管理用户认证信息。 这里以 Pinia 为例:

// src/stores/index.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: localStorage.getItem('token') || null,
    user: null, // 可以存储用户信息
    permissions: [] // 存储用户权限
  }),
  getters: {
    isLoggedIn: (state) => !!state.token
  },
  actions: {
    setToken(token) {
      this.token = token;
      localStorage.setItem('token', token);
    },
    setUser(user) {
      this.user = user;
    },
    setPermissions(permissions) {
      this.permissions = permissions;
    },
    clearAuth() {
      this.token = null;
      this.user = null;
      this.permissions = [];
      localStorage.removeItem('token');
    }
  }
})

在这个示例中,我们使用 Pinia 创建了一个 auth store,用于存储用户的 token、用户信息和权限信息。isLoggedIn getter 用于判断用户是否已登录。 setToken, setUser, setPermissions 用于更新 state。 clearAuth 用于登出。

4.5. 路由守卫模块

src/router/index.js 文件中,添加路由守卫:

import router from './index';
import { useAuthStore } from '../stores'; // Pinia
import axios from 'axios';

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore();

  // 1. 检查路由是否需要登录
  if (to.meta.requiresAuth) {
    // 2. 检查用户是否已登录
    if (!authStore.isLoggedIn) {
      // 3. 如果未登录,则跳转到登录页面
      next({ name: 'Login', query: { redirect: to.fullPath } }); // 记录跳转前的路径
    } else {
      // 4. 如果已登录,但尚未加载权限信息,则加载权限信息 (首次登录或刷新页面)
      if (authStore.permissions.length === 0) {
        try {
          // 从后端 API 获取用户权限信息
          const response = await axios.get('/api/user/permissions', {
            headers: {
              Authorization: `Bearer ${authStore.token}`
            }
          });
          const permissions = response.data;

          // 将权限信息存储到 store 中
          authStore.setPermissions(permissions);

          // 5. 动态添加路由
          addDynamicRoutes(permissions);

          // 6. 重新执行导航 (replace: true 避免导航历史记录中出现重复条目)
          next({ ...to, replace: true });
        } catch (error) {
          // 7. 如果获取权限信息失败,则跳转到登录页面或错误页面
          console.error('获取权限信息失败:', error);
          authStore.clearAuth();
          next({ name: 'Login', query: { redirect: to.fullPath } });
        }
      } else {
        // 8. 如果已登录且已加载权限信息,则检查用户是否具有访问权限
        if (hasPermission(to, authStore.permissions)) {
          // 9. 如果有权限,则允许访问
          next();
        } else {
          // 10. 如果没有权限,则跳转到 403 页面或首页
          next({ name: 'NotFound' }); // 404 或者 403
        }
      }
    }
  } else {
    // 11. 如果路由不需要登录,则允许访问
    next();
  }
});

// 动态添加路由
function addDynamicRoutes(permissions) {
  const dynamicRoutes = generateRoutes(permissions); // 根据权限生成路由
  dynamicRoutes.forEach(route => {
    router.addRoute(route);
  });
}

// 根据权限生成路由
function generateRoutes(permissions) {
  const routes = [];

  // 示例:根据权限动态添加路由
  if (permissions.includes('admin')) {
    routes.push({
      path: '/admin',
      name: 'Admin',
      component: () => import('../views/Admin.vue'),
      meta: {
        requiresAuth: true,
        permissions: ['admin'] // 路由所需的权限 (可选)
      }
    });
  }

  if (permissions.includes('editor')) {
    routes.push({
      path: '/editor',
      name: 'Editor',
      component: () => import('../views/Editor.vue'),
      meta: {
        requiresAuth: true,
        permissions: ['editor'] // 路由所需的权限 (可选)
      }
    });
  }

  return routes;
}

// 检查用户是否具有访问权限
function hasPermission(to, permissions) {
  // 1. 如果路由不需要权限,则允许访问
  if (!to.meta.permissions) {
    return true;
  }

  // 2. 检查用户是否具有路由所需的权限
  return to.meta.permissions.every(permission => permissions.includes(permission));
}

这段代码实现了以下逻辑:

  1. beforeEach 路由守卫: 在每次路由导航之前执行。
  2. 检查路由是否需要登录: 通过 to.meta.requiresAuth 判断。
  3. 检查用户是否已登录: 通过 authStore.isLoggedIn 判断。
  4. 如果未登录: 跳转到登录页面,并记录跳转前的路径,方便登录后跳转回原页面。
  5. 如果已登录,但尚未加载权限信息: 从后端 API 获取用户权限信息,并存储到 store 中。 然后调用 addDynamicRoutes 动态添加路由。
  6. addDynamicRoutes 函数: 遍历动态路由,并使用 router.addRoute 将其添加到路由实例中。
  7. generateRoutes 函数: 根据用户的权限信息生成路由表。 这里只是一个示例,实际项目中需要根据业务需求进行调整。
  8. hasPermission 函数: 检查用户是否具有访问当前路由的权限。 如果路由定义了 meta.permissions,则只有拥有所有这些权限的用户才能访问。

4.6. 登录组件

创建一个 src/views/Login.vue 组件:

<template>
  <div>
    <h1>Login</h1>
    <input type="text" v-model="username" placeholder="Username" />
    <input type="password" v-model="password" placeholder="Password" />
    <button @click="login">Login</button>
  </div>
</template>

<script>
import { ref } from 'vue';
import { useAuthStore } from '../stores'; // Pinia
import { useRouter, useRoute } from 'vue-router';
import axios from 'axios';

export default {
  setup() {
    const username = ref('');
    const password = ref('');
    const authStore = useAuthStore();
    const router = useRouter();
    const route = useRoute();

    const login = async () => {
      try {
        // 调用后端 API 进行登录
        const response = await axios.post('/api/login', {
          username: username.value,
          password: password.value
        });

        const { token, user, permissions } = response.data;

        // 将用户信息存储到 store 中
        authStore.setToken(token);
        authStore.setUser(user);
        authStore.setPermissions(permissions);

        // 跳转到登录前的页面或首页
        const redirect = route.query.redirect || '/';
        router.push(redirect);

      } catch (error) {
        console.error('登录失败:', error);
        alert('登录失败,请检查用户名和密码。');
      }
    };

    return {
      username,
      password,
      login
    };
  }
};
</script>

这个组件包含一个简单的登录表单,用户输入用户名和密码后,点击登录按钮,会调用后端 API 进行登录,并将用户信息存储到 store 中,最后跳转到登录前的页面或首页。

4.7. Home组件
创建一个 src/views/Home.vue 组件:

<template>
  <div>
    <h1>Home</h1>
    <p>Welcome to the home page!</p>
  </div>
</template>

4.8. Admin组件
创建一个 src/views/Admin.vue 组件:

<template>
  <div>
    <h1>Admin Page</h1>
    <p>Only accessible to admins.</p>
  </div>
</template>

4.9. Editor组件
创建一个 src/views/Editor.vue 组件:

<template>
  <div>
    <h1>Editor Page</h1>
    <p>Only accessible to editors.</p>
  </div>
</template>

5. 后端 API (模拟)

为了测试我们的前端代码,我们需要模拟一个后端 API。可以使用 Node.js 和 Express.js 快速搭建一个简单的 API 服务器。

// server.js
const express = require('express');
const app = express();
const port = 3000;

app.use(express.json());

app.post('/api/login', (req, res) => {
  const { username, password } = req.body;

  // 模拟用户认证
  if (username === 'admin' && password === 'password') {
    const token = 'admin_token';
    const user = { id: 1, username: 'admin' };
    const permissions = ['admin', 'editor']; // 模拟管理员权限
    res.json({ token, user, permissions });
  } else if (username === 'editor' && password === 'password') {
    const token = 'editor_token';
    const user = { id: 2, username: 'editor' };
    const permissions = ['editor']; // 模拟编辑权限
    res.json({ token, user, permissions });
  } else {
    res.status(401).json({ message: 'Invalid credentials' });
  }
});

app.get('/api/user/permissions', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];

  // 模拟权限验证
  if (token === 'admin_token') {
    res.json(['admin', 'editor']);
  } else if (token === 'editor_token') {
    res.json(['editor']);
  } else {
    res.status(401).json({ message: 'Unauthorized' });
  }
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

6. 测试

  1. 启动后端 API 服务器 (node server.js)。
  2. 启动 Vue 项目 (npm run serve)。
  3. 在浏览器中访问 http://localhost:8080
  4. 如果未登录,则会自动跳转到登录页面。
  5. 使用 "admin" 用户名和 "password" 密码登录,可以访问 /admin/editor 页面。
  6. 使用 "editor" 用户名和 "password" 密码登录,只能访问 /editor 页面。

7. 优化和扩展

  • 权限缓存: 将权限信息缓存到 LocalStorage 或 SessionStorage 中,避免每次刷新页面都重新获取权限信息。 注意要处理好缓存失效的问题。
  • 更细粒度的权限控制: 可以使用 RBAC (Role-Based Access Control) 或 ABAC (Attribute-Based Access Control) 模型来实现更细粒度的权限控制。
  • 前端权限验证: 除了路由守卫,还可以在组件内部进行权限验证,例如根据权限控制按钮的显示和隐藏。
  • 后端权限验证: 前端的权限验证只是第一道防线,后端也需要进行权限验证,以确保数据的安全性。
  • 路由配置化: 将路由配置信息存储到数据库或配置文件中,方便管理和维护。
  • 错误处理: 添加错误处理机制,例如当用户没有权限访问某个页面时,显示 403 页面。

总结

  • 动态权限路由的核心: 权限控制是现代 Web 应用的关键,核心在于根据用户权限动态生成路由表,利用路由守卫控制访问。
  • 构建步骤的关键点: 从用户认证模块到路由守卫模块,每一步都至关重要,特别是动态添加路由和权限验证的逻辑。
  • 持续优化系统架构: 权限缓存、细粒度权限控制和前后端权限验证是持续优化的方向,保证系统的安全性、可维护性和可扩展性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注