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: 作为状态管理工具,用于存储用户认证信息和权限信息。如果项目规模较小,也可以考虑使用
reactive
或ref
来管理全局状态。 - 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));
}
这段代码实现了以下逻辑:
beforeEach
路由守卫: 在每次路由导航之前执行。- 检查路由是否需要登录: 通过
to.meta.requiresAuth
判断。 - 检查用户是否已登录: 通过
authStore.isLoggedIn
判断。 - 如果未登录: 跳转到登录页面,并记录跳转前的路径,方便登录后跳转回原页面。
- 如果已登录,但尚未加载权限信息: 从后端 API 获取用户权限信息,并存储到 store 中。 然后调用
addDynamicRoutes
动态添加路由。 addDynamicRoutes
函数: 遍历动态路由,并使用router.addRoute
将其添加到路由实例中。generateRoutes
函数: 根据用户的权限信息生成路由表。 这里只是一个示例,实际项目中需要根据业务需求进行调整。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. 测试
- 启动后端 API 服务器 (
node server.js
)。 - 启动 Vue 项目 (
npm run serve
)。 - 在浏览器中访问
http://localhost:8080
。 - 如果未登录,则会自动跳转到登录页面。
- 使用 "admin" 用户名和 "password" 密码登录,可以访问
/admin
和/editor
页面。 - 使用 "editor" 用户名和 "password" 密码登录,只能访问
/editor
页面。
7. 优化和扩展
- 权限缓存: 将权限信息缓存到 LocalStorage 或 SessionStorage 中,避免每次刷新页面都重新获取权限信息。 注意要处理好缓存失效的问题。
- 更细粒度的权限控制: 可以使用 RBAC (Role-Based Access Control) 或 ABAC (Attribute-Based Access Control) 模型来实现更细粒度的权限控制。
- 前端权限验证: 除了路由守卫,还可以在组件内部进行权限验证,例如根据权限控制按钮的显示和隐藏。
- 后端权限验证: 前端的权限验证只是第一道防线,后端也需要进行权限验证,以确保数据的安全性。
- 路由配置化: 将路由配置信息存储到数据库或配置文件中,方便管理和维护。
- 错误处理: 添加错误处理机制,例如当用户没有权限访问某个页面时,显示 403 页面。
总结
- 动态权限路由的核心: 权限控制是现代 Web 应用的关键,核心在于根据用户权限动态生成路由表,利用路由守卫控制访问。
- 构建步骤的关键点: 从用户认证模块到路由守卫模块,每一步都至关重要,特别是动态添加路由和权限验证的逻辑。
- 持续优化系统架构: 权限缓存、细粒度权限控制和前后端权限验证是持续优化的方向,保证系统的安全性、可维护性和可扩展性。