企业级后台系统的核心功能,覆盖从登录到权限控制的全流程
前言
在后台管理系统中,权限控制是绕不开的核心需求。无论是简单的内部系统,还是复杂的 SaaS 平台,一套完善的权限方案都是系统安全性和可扩展性的基石。
本文将从零开始,手把手带你实现一套完整的 RBAC 权限管理系统,涵盖 菜单权限 、按钮权限 和 数据权限 三大核心模块。文章基于 Vue3 + TypeScript + Vue Router 4 + Pinia 技术栈,所有代码均已实战验证。
一、RBAC 权限模型详解
1.1 什么是 RBAC?
RBAC(Role-Based Access Control,基于角色的访问控制)是目前最主流的权限管理模型。核心思想是:用户 → 角色 → 权限,通过角色作为中间层,将用户与权限解耦。
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 用户 │────▶│ 角色 │────▶│ 权限 │
└─────────┘ └─────────┘ └─────────┘
1.2 核心概念
| 概念 | 说明 | 示例 |
|---|---|---|
| 用户 | 系统使用者 | admin、zhangsan |
| 角色 | 权限的集合 | 超级管理员、运营专员 |
| 权限 | 可执行的操作 | 用户管理、删除按钮、查看数据 |
1.3 权限粒度划分
- 菜单权限:控制用户能看到哪些菜单/页面
- 按钮权限:控制用户能使用哪些按钮/操作
- 数据权限:控制用户能访问哪些数据范围(本部门、全部、仅本人)
二、前端权限方案设计
2.1 整体架构流程图
text
用户登录 ──▶ 后端返回 token + roles ──▶ 前端存储 token
│
▼
根据 roles 获取权限菜单 + 权限标识
│
▼
动态生成路由 + 渲染菜单树
│
▼
路由守卫拦截 + 按钮权限指令
│
▼
数据权限过滤请求参数
2.2 技术选型
| 模块 | 技术 |
|---|---|
| 框架 | Vue 3 + Composition API |
| 路由 | Vue Router 4 |
| 状态管理 | Pinia |
| HTTP 请求 | Axios |
| UI 组件库 | Element Plus / Ant Design Vue |
| 类型支持 | TypeScript |
2.3 后端数据结构约定
假设后端返回的权限数据结构如下:
typescript
// 登录响应
interface LoginResponse {
token: string;
userInfo: UserInfo;
}
interface UserInfo {
id: string;
username: string;
roles: string[]; // ['admin', 'editor']
}
// 权限菜单响应
interface PermissionMenu {
id: string;
name: string;
path: string;
component: string;
icon?: string;
children?: PermissionMenu[];
meta: {
title: string;
permission?: string; // 菜单对应的权限标识
};
}
// 按钮权限响应
interface ButtonPermission {
code: string; // 权限标识,如 'user:add'
name: string; // 权限名称
}
三、项目初始化与基础配置
3.1 创建项目
bash
npm create vue@latest vue3-permission-demo
# 选择 TypeScript、Vue Router、Pinia
cd vue3-permission-demo
npm install
npm install axios element-plus
3.2 目录结构设计
text
src/
├── api/ # API 接口
│ ├── auth.ts # 登录/登出
│ ├── permission.ts # 权限相关
│ └── types/ # 接口类型定义
├── directives/ # 自定义指令(按钮权限)
│ └── permission.ts
├── layouts/ # 布局组件
│ ├── DefaultLayout.vue
│ └── Sidebar.vue
├── router/ # 路由配置
│ ├── index.ts # 路由实例
│ ├── routes.ts # 静态路由
│ └── permission.ts # 权限守卫
├── stores/ # Pinia 状态
│ ├── user.ts # 用户状态
│ ├── permission.ts # 权限状态
│ └── app.ts # 应用配置
├── utils/ # 工具函数
│ ├── request.ts # Axios 封装
│ └── storage.ts # 本地存储
└── views/ # 页面视图
四、登录认证与 Token 管理
4.1 Axios 封装
typescript
// utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/stores/user';
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore();
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, data, message } = response.data;
if (code === 200) {
return data;
}
ElMessage.error(message || '请求失败');
return Promise.reject(new Error(message));
},
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore();
userStore.logout();
window.location.href = '/login';
}
ElMessage.error(error.message || '网络错误');
return Promise.reject(error);
}
);
export default service;
4.2 登录 API
typescript
// api/auth.ts
import request from '@/utils/request';
import type { LoginParams, LoginResponse, UserInfo } from './types';
export const loginApi = (data: LoginParams): Promise<LoginResponse> => {
return request.post('/auth/login', data);
};
export const getUserInfoApi = (): Promise<UserInfo> => {
return request.get('/auth/user-info');
};
export const logoutApi = (): Promise<void> => {
return request.post('/auth/logout');
};
4.3 用户 Store 实现
typescript
// stores/user.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { loginApi, getUserInfoApi, logoutApi } from '@/api/auth';
import { usePermissionStore } from './permission';
import router from '@/router';
export const useUserStore = defineStore('user', () => {
const token = ref<string>(localStorage.getItem('token') || '');
const userInfo = ref<any>(null);
const roles = ref<string[]>([]);
const setToken = (newToken: string) => {
token.value = newToken;
localStorage.setItem('token', newToken);
};
const login = async (username: string, password: string) => {
const res = await loginApi({ username, password });
setToken(res.token);
await getUserInfo();
return res;
};
const getUserInfo = async () => {
const res = await getUserInfoApi();
userInfo.value = res;
roles.value = res.roles;
// 获取权限后,生成动态路由
const permissionStore = usePermissionStore();
await permissionStore.generateRoutes(roles.value);
return res;
};
const logout = async () => {
try {
await logoutApi();
} finally {
token.value = '';
userInfo.value = null;
roles.value = [];
localStorage.removeItem('token');
// 重置路由
const permissionStore = usePermissionStore();
permissionStore.resetRoutes();
router.push('/login');
}
};
return {
token,
userInfo,
roles,
login,
getUserInfo,
logout,
setToken,
};
});
五、菜单权限实现(动态路由)
5.1 路由结构设计
typescript
// router/routes.ts
import type { RouteRecordRaw } from 'vue-router';
// 静态路由(无需权限)
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录', hidden: true },
},
{
path: '/404',
name: '404',
component: () => import('@/views/error/404.vue'),
meta: { title: '404', hidden: true },
},
];
// 动态路由(需要权限)
export const asyncRoutes: RouteRecordRaw[] = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { title: '仪表盘', icon: 'dashboard', permission: 'dashboard:view' },
children: [
{
path: 'index',
name: 'DashboardIndex',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '工作台', permission: 'dashboard:view' },
},
],
},
{
path: '/system',
name: 'System',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { title: '系统管理', icon: 'setting', permission: 'system:view' },
children: [
{
path: 'user',
name: 'UserManage',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '用户管理', permission: 'system:user:view' },
},
{
path: 'role',
name: 'RoleManage',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', permission: 'system:role:view' },
},
],
},
];
5.2 权限 Store(路由过滤核心)
typescript
// stores/permission.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { asyncRoutes, constantRoutes } from '@/router/routes';
import type { RouteRecordRaw } from 'vue-router';
import router from '@/router';
// 递归过滤路由
function filterRoutes(routes: RouteRecordRaw[], permissions: string[]): RouteRecordRaw[] {
return routes.filter(route => {
// 检查当前路由权限
const permission = route.meta?.permission as string;
if (permission && !permissions.includes(permission)) {
return false;
}
// 递归过滤子路由
if (route.children) {
route.children = filterRoutes(route.children, permissions);
// 如果子路由全部被过滤,则当前路由也无意义
if (route.children.length === 0 && !route.meta?.alwaysShow) {
return false;
}
}
return true;
});
}
export const usePermissionStore = defineStore('permission', () => {
const routes = ref<RouteRecordRaw[]>([]);
// 获取所有可访问路由(包含静态路由)
const accessibleRoutes = computed(() => constantRoutes.concat(routes.value));
// 获取菜单路由(用于侧边栏渲染,排除 hidden 路由)
const menuRoutes = computed(() => {
return accessibleRoutes.value.filter(route => !route.meta?.hidden);
});
// 生成动态路由
const generateRoutes = async (roles: string[]) => {
// 获取用户权限标识列表(由后端返回)
const permissions = await getUserPermissions(roles);
// 根据权限过滤动态路由
const filteredRoutes = filterRoutes(asyncRoutes, permissions);
routes.value = filteredRoutes;
// 动态添加路由
filteredRoutes.forEach(route => {
router.addRoute(route);
});
// 添加 404 兜底路由
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue'),
});
return filteredRoutes;
};
// 重置路由(登出时调用)
const resetRoutes = () => {
routes.value = [];
// 移除所有动态添加的路由
const existingRoutes = router.getRoutes();
existingRoutes.forEach(route => {
if (route.name && route.name !== 'Login' && route.name !== '404') {
router.removeRoute(route.name);
}
});
};
return {
routes,
accessibleRoutes,
menuRoutes,
generateRoutes,
resetRoutes,
};
});
// 模拟获取用户权限(实际应该调用 API)
async function getUserPermissions(roles: string[]): Promise<string[]> {
// 可以根据角色从后端获取权限标识列表
// 这里模拟数据
if (roles.includes('admin')) {
return ['dashboard:view', 'system:view', 'system:user:view', 'system:role:view'];
}
if (roles.includes('editor')) {
return ['dashboard:view'];
}
return [];
}
5.3 路由守卫实现
typescript
// router/permission.ts
import router from './index';
import { useUserStore } from '@/stores/user';
import { usePermissionStore } from '@/stores/permission';
import { ElMessage } from 'element-plus';
const whiteList = ['/login', '/404'];
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const permissionStore = usePermissionStore();
// 设置页面标题
document.title = `${to.meta.title || '权限系统'} | Vue3 Admin`;
if (userStore.token) {
if (to.path === '/login') {
// 已登录,跳转到首页
next({ path: '/' });
} else {
// 检查是否已获取用户信息
if (!userStore.userInfo) {
try {
await userStore.getUserInfo();
// 重定向到目标路由(避免动态路由未完全添加的问题)
next({ ...to, replace: true });
} catch (error) {
// token 无效,清除并跳转登录
await userStore.logout();
next(`/login?redirect=${to.path}`);
}
} else {
next();
}
}
} else {
// 未登录
if (whiteList.includes(to.path)) {
next();
} else {
next(`/login?redirect=${to.path}`);
}
}
});
六、按钮权限实现(自定义指令)
6.1 按钮权限指令
typescript
// directives/permission.ts
import type { Directive, DirectiveBinding } from 'vue';
import { useUserStore } from '@/stores/user';
// 权限指令 v-permission="'user:add'"
export const permissionDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
const userStore = useUserStore();
const permissions = userStore.userInfo?.permissions || [];
if (value && !permissions.includes(value)) {
// 没有权限,移除元素或添加禁用样式
if (el.parentNode) {
el.parentNode.removeChild(el);
}
// 或者隐藏元素
// el.style.display = 'none';
}
},
};
// 按钮级别权限函数式使用
export function hasPermission(permission: string): boolean {
const userStore = useUserStore();
const permissions = userStore.userInfo?.permissions || [];
return permissions.includes(permission);
}
6.2 注册全局指令
typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from 'pinia';
import { permissionDirective } from './directives/permission';
const app = createApp(App);
app.use(createPinia());
app.use(router);
// 注册权限指令
app.directive('permission', permissionDirective);
app.mount('#app');
6.3 使用示例
vue
<template>
<div>
<!-- 指令方式 -->
<el-button v-permission="'user:add'" type="primary" @click="handleAdd">
新增用户
</el-button>
<!-- 函数式判断 -->
<el-button
v-if="hasPermission('user:edit')"
type="warning"
@click="handleEdit"
>
编辑
</el-button>
<el-button
v-if="hasPermission('user:delete')"
type="danger"
@click="handleDelete"
>
删除
</el-button>
</div>
</template>
<script setup lang="ts">
import { hasPermission } from '@/directives/permission';
const handleAdd = () => {
console.log('新增用户');
};
</script>
七、数据权限实现
7.1 数据权限方案设计
数据权限通常在请求层实现,前端需要将当前用户的数据权限范围传递给后端。
常见数据权限类型:
- ALL:全部数据
- DEPT:本部门数据
- DEPT_AND_SUB:本部门及子部门
- SELF:仅本人数据
7.2 在请求中注入数据权限
typescript
// utils/request.ts 中添加数据权限拦截器
import { useUserStore } from '@/stores/user';
// 数据权限枚举
export enum DataScope {
ALL = 'ALL',
DEPT = 'DEPT',
DEPT_AND_SUB = 'DEPT_AND_SUB',
SELF = 'SELF',
}
// 在请求拦截器中添加数据权限参数
service.interceptors.request.use((config) => {
const userStore = useUserStore();
// 添加用户信息到请求头,供后端做数据权限过滤
if (userStore.userInfo) {
config.headers['X-User-Id'] = userStore.userInfo.id;
config.headers['X-Dept-Id'] = userStore.userInfo.deptId;
config.headers['X-Data-Scope'] = userStore.userInfo.dataScope || DataScope.DEPT;
}
return config;
});
7.3 数据权限 Hook
typescript
// composables/useDataPermission.ts
import { computed } from 'vue';
import { useUserStore } from '@/stores/user';
import { DataScope } from '@/utils/request';
export function useDataPermission() {
const userStore = useUserStore();
const currentUserDeptId = computed(() => userStore.userInfo?.deptId);
const currentUserId = computed(() => userStore.userInfo?.id);
const dataScope = computed(() => userStore.userInfo?.dataScope);
// 判断是否能查看某条数据
const canViewData = (data: { userId?: string; deptId?: string }): boolean => {
switch (dataScope.value) {
case DataScope.ALL:
return true;
case DataScope.DEPT:
return data.deptId === currentUserDeptId.value;
case DataScope.SELF:
return data.userId === currentUserId.value;
case DataScope.DEPT_AND_SUB:
// 需要后端判断子部门,前端简化为部门匹配
return data.deptId === currentUserDeptId.value;
default:
return false;
}
};
// 构建数据权限查询参数
const buildDataPermissionParams = () => {
return {
dataScope: dataScope.value,
userId: currentUserId.value,
deptId: currentUserDeptId.value,
};
};
return {
currentUserDeptId,
currentUserId,
dataScope,
canViewData,
buildDataPermissionParams,
};
}
7.4 列表页面使用示例
vue
<template>
<div>
<el-table :data="tableData" v-loading="loading">
<el-table-column prop="username" label="用户名" />
<el-table-column prop="deptName" label="部门" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button
v-if="canEdit(row)"
type="text"
@click="handleEdit(row)"
>
编辑
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useDataPermission } from '@/composables/useDataPermission';
import { getUserList } from '@/api/user';
const { canViewData, buildDataPermissionParams } = useDataPermission();
const loading = ref(false);
const tableData = ref([]);
// 获取列表时传递数据权限参数
const fetchData = async () => {
loading.value = true;
try {
const params = {
...buildDataPermissionParams(),
page: 1,
pageSize: 10,
};
const res = await getUserList(params);
// 前端二次过滤(可选,主要靠后端)
tableData.value = res.list.filter(item => canViewData(item));
} finally {
loading.value = false;
}
};
// 判断是否可编辑
const canEdit = (row: any) => {
const { dataScope, currentUserId } = useDataPermission();
// 数据权限 + 按钮权限组合判断
if (dataScope.value === 'SELF' && row.userId !== currentUserId.value) {
return false;
}
return hasPermission('user:edit');
};
onMounted(() => {
fetchData();
});
</script>
八、侧边栏菜单渲染
8.1 递归菜单组件
vue
<!-- layouts/components/SidebarItem.vue -->
<template>
<template v-if="hasChildren(item)">
<el-sub-menu :index="item.path">
<template #title>
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<span>{{ item.meta?.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
/>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="resolvePath(item.path)" @click="handleNavigate(item)">
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<template #title>{{ item.meta?.title }}</template>
</el-menu-item>
</template>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
const props = defineProps<{
item: RouteRecordRaw;
basePath?: string;
}>();
const router = useRouter();
const hasChildren = (route: RouteRecordRaw) => {
return route.children && route.children.length > 0;
};
const resolvePath = (routePath: string) => {
if (props.basePath) {
return `${props.basePath}/${routePath}`.replace(//+/g, '/');
}
return routePath;
};
const handleNavigate = (item: RouteRecordRaw) => {
const fullPath = resolvePath(item.path);
router.push(fullPath);
};
</script>
8.2 侧边栏主组件
vue
<!-- layouts/Sidebar.vue -->
<template>
<div class="sidebar">
<div class="logo">
<img src="/logo.svg" alt="logo" />
<span v-if="!isCollapse">权限管理系统</span>
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:unique-opened="true"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
router
>
<sidebar-item
v-for="route in menuRoutes"
:key="route.path"
:item="route"
/>
</el-menu>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { usePermissionStore } from '@/stores/permission';
import { useAppStore } from '@/stores/app';
import SidebarItem from './SidebarItem.vue';
const route = useRoute();
const permissionStore = usePermissionStore();
const appStore = useAppStore();
const menuRoutes = computed(() => permissionStore.menuRoutes);
const isCollapse = computed(() => appStore.sidebarCollapse);
const activeMenu = computed(() => {
const { path, meta } = route;
if (meta.activeMenu) {
return meta.activeMenu as string;
}
return path;
});
</script>
九、完整登录页面实现
vue
<!-- views/login/index.vue -->
<template>
<div class="login-container">
<el-form
ref="formRef"
:model="form"
:rules="rules"
class="login-form"
>
<h2 class="title">权限管理系统</h2>
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="用户名"
prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="密码"
prefix-icon="Lock"
size="large"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-btn"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
<div class="demo-tips">
<p>演示账号:</p>
<p>超级管理员:admin / 123456</p>
<p>普通用户:editor / 123456</p>
</div>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/stores/user';
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
const formRef = ref();
const loading = ref(false);
const form = reactive({
username: 'admin',
password: '123456',
});
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
const handleLogin = async () => {
if (!formRef.value) return;
await formRef.value.validate();
loading.value = true;
try {
await userStore.login(form.username, form.password);
ElMessage.success('登录成功');
const redirect = route.query.redirect as string || '/';
router.push(redirect);
} catch (error: any) {
ElMessage.error(error.message || '登录失败');
} finally {
loading.value = false;
}
};
</script>
<style scoped lang="scss">
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.login-form {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.title {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.login-btn {
width: 100%;
}
.demo-tips {
margin-top: 20px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
color: #666;
p {
margin: 4px 0;
}
}
}
}
</style>
十、完整项目源码结构
10.1 API 类型定义
typescript
// api/types/index.ts
export interface LoginParams {
username: string;
password: string;
}
export interface LoginResponse {
token: string;
userInfo: UserInfo;
}
export interface UserInfo {
id: string;
username: string;
nickname: string;
avatar: string;
roles: string[];
permissions: string[];
deptId: string;
deptName: string;
dataScope: 'ALL' | 'DEPT' | 'DEPT_AND_SUB' | 'SELF';
}
export interface PermissionMenu {
id: string;
name: string;
path: string;
component: string;
parentId: string | null;
icon?: string;
sort: number;
meta: {
title: string;
permission: string;
hidden?: boolean;
alwaysShow?: boolean;
};
children?: PermissionMenu[];
}
10.2 环境变量配置
bash
# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=权限管理系统
10.3 入口文件完整配置
typescript
// main.ts
import { createApp } from 'vue';
import pinia from './stores';
import router from './router';
import App from './App.vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import { permissionDirective } from './directives/permission';
const app = createApp(App);
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(pinia);
app.use(router);
app.use(ElementPlus);
app.directive('permission', permissionDirective);
// 路由守卫
import './router/permission';
app.mount('#app');
十一、常见问题与最佳实践
11.1 动态路由刷新 404 问题
问题:页面刷新后,动态添加的路由丢失,导致 404。
解决方案:在路由守卫中判断,如果用户已登录但没有 routes,重新生成。
typescript
// router/permission.ts 中添加
router.beforeEach(async (to, from, next) => {
// ... 省略其他代码
if (userStore.token) {
if (userStore.userInfo) {
// 检查动态路由是否存在
const permissionStore = usePermissionStore();
if (permissionStore.routes.length === 0 && to.path !== '/login') {
await permissionStore.generateRoutes(userStore.roles);
next({ ...to, replace: true });
return;
}
next();
} else {
// ... 获取用户信息
}
}
});
11.2 性能优化建议
- 路由懒加载:所有页面组件使用动态 import
- 权限数据缓存:用户权限信息存储在 localStorage 中,避免重复请求
- 菜单渲染优化 :使用
v-memo优化递归组件渲染
11.3 安全注意事项
- 前端权限只做 UI 展示:真正的权限校验必须在后端执行
- Token 安全存储:敏感信息避免使用 localStorage,可使用 httpOnly Cookie
- 路由守卫不能绕过:所有页面跳转都会经过守卫,但后端仍需鉴权
总结
本文完整实现了一套基于 Vue3 的 RBAC 权限管理系统,涵盖了:
- ✅ RBAC 权限模型的核心概念
- ✅ 前端权限架构设计与流程
- ✅ Token 登录认证完整实现
- ✅ 动态菜单/路由权限(后端驱动)
- ✅ 按钮级权限(自定义指令 + 函数式)
- ✅ 数据权限(请求拦截 + 组合式函数)
- ✅ 路由守卫完整实现
- ✅ 侧边栏递归菜单组件
这套方案已在多个企业级项目中验证,具有良好的扩展性和可维护性。你可以根据实际业务需求,在此基础上增加更多功能,如:权限缓存策略、多租户支持、API 级别权限校验等。
本文为技术分享,欢迎交流讨论。如有疑问,请在评论区留言。