需求背景:
( 公司希望在现运行的后台系统上, 满足不同角色的划分,保护隐私数据 ,实现权限控制, 于是我基于诺依系统的权限管理功能, 借鉴了思路)
- 数据隔离:按角色划分数据与功能可见范围,如普通员工无权查看管理层报表。
- 操作限制:基于角色职责管控按钮级操作,如禁止实习生执行删除、审批等高危操作。
- 体验优化:动态隐藏无权限的菜单、按钮,减少无效操作,降低学习成本。
- 路由安全:拦截通过手动修改 URL 访问无权限页面的行为,避免前端报错和后端无效请求。
项目实现部分截图
一、权限数据的初始化流程
前端权限系统的运转始于权限数据的加载,这一过程集中在用户登录后的初始化阶段。诺依前端采用 Vue 生态,通过 Vuex 管理全局状态,权限数据的流转路径清晰可控。
- 登录成功后的权限加载触发
当用户登录请求得到后端响应(包含 token)后,前端会立即调用权限初始化方法:
javascript
// src/store/modules/user.js
actions: {
// 登录动作
login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const { token } = response.data;
// 存储token
setToken(token);
commit('SET_TOKEN', token);
resolve();
}).catch(error => {
reject(error);
});
});
},
// 加载用户权限信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const { user, roles, permissions } = response.data;
// 存储用户信息、角色、权限标识
commit('SET_USER', user);
commit('SET_ROLES', roles);
commit('SET_PERMISSIONS', permissions);
resolve(response.data);
}).catch(error => {
reject(error);
});
});
}
}
这里的getInfo接口会返回三组关键数据:用户基本信息、角色列表(如["admin", "user"])、权限标识集合(如["system:user:add", "system:user:edit"])。
- 动态路由的生成与注入
权限标识加载完成后,下一步是生成可访问的路由配置。诺依将路由分为 "常量路由"(如登录页、404 页)和 "动态路由"(需权限控制的业务页面):
javascript
// src/store/modules/permission.js
actions: {
generateRoutes({ commit, state }) {
return new Promise(resolve => {
// 调用后端接口获取菜单树
getRouters().then(response => {
const asyncRoutes = response.data;
// 将后端返回的菜单数据转换为前端路由配置
const accessedRoutes = filterAsyncRoutes(asyncRoutes);
commit('SET_ROUTES', accessedRoutes);
resolve(accessedRoutes);
});
});
}
}
// 菜单转路由的核心函数
function filterAsyncRoutes(routes) {
const res = [];
routes.forEach(route => {
const tmp = { ...route };
// 组件路径转换(诺依约定组件路径格式为"module/component")
if (tmp.component) {
// 目录
if (tmp.component === 'Layout') {
tmp.component = () => import("@/views/index");;
// 父级菜单
} else if (tmp.component === "ParentView") {
tmp.component = ParentView;
// 子菜单
}else {
tmp.component = loadView(tmp.component); // 懒加载业务组件
}
}
// 递归处理子菜单
if (tmp.children && tmp.children.length > 0) {
tmp.children = filterAsyncRoutes(tmp.children);
}
res.push(tmp);
});
return res;
}
// 组件懒加载函数
export const loadView = (view) => {
return () => import(`@/views/${view}`);
};
转换完成的路由会通过router.addRoutes方法注入 Vue Router,此时用户才能通过路由访问对应页面。
二、路由守卫:权限校验
即使动态路由已注入,仍需防止用户通过手动修改 URL 访问无权限页面。诺依通过全局路由守卫实现实时权限校验:
javascript
// src/permission.js
router.beforeEach(async(to, from, next) => {
// 从本地存储获取token
const hasToken = getToken();
if (hasToken) {
if (to.path === '/login') {
// 已登录跳转首页
next({ path: '/' });
} else {
// 判断是否已加载权限信息
const hasRoles = store.getters.roles && store.getters.roles.length > 0;
if (hasRoles) {
next();
} else {
try {
// 加载权限信息
const { roles } = await store.dispatch('user/getInfo');
// 生成动态路由
const accessRoutes = await store.dispatch('permission/generateRoutes');
// 注入路由
router.addRoutes(accessRoutes);
// 重定向至目标路由(确保路由已生效)
next({ ...to, replace: true });
} catch (error) {
// 权限加载失败,清除token并跳转登录页
await store.dispatch('user/resetToken');
Message.error(error || '权限加载失败');
next(`/login?redirect=${to.path}`);
}
}
}
} else {
// 未登录状态处理
if (whiteList.indexOf(to.path) !== -1) {
// 白名单路由直接放行
next();
} else {
// 非白名单路由跳转登录页
next(`/login?redirect=${to.path}`);
}
}
});
这段代码构成了权限校验的核心逻辑:未登录用户强制跳转登录页,已登录用户需完成权限加载与路由注入后才能访问页面。
三、动态菜单渲染:从数据到 UI 的转换
菜单是权限最直观的体现,诺依通过递归组件将权限菜单数据转换为可视化导航:
- 菜单组件的递归渲染
javascript
<!-- src/layout/components/Sidebar/SidebarMenu.vue -->
<template>
<div class="menu-container">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
mode="vertical"
>
<template v-for="(route, index) in routes">
<!-- 有子菜单的项 -->
<el-submenu
v-if="route.children && route.children.length > 0"
:index="route.path"
:key="index"
>
<template #title>
<svg-icon :icon-class="route.icon" />
<span>{{ route.meta.title }}</span>
</template>
<!-- 递归渲染子菜单 -->
<sidebar-menu
:routes="route.children"
:base-path="resolvePath(route.path)"
:is-collapsed="isCollapse"
/>
</el-submenu>
<!-- 无子菜单的项 -->
<el-menu-item
v-else
:index="route.path"
:key="index"
@click="handleClick(route)"
>
<svg-icon :icon-class="route.icon" />
<span>{{ route.meta.title }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script>
export default {
name: 'SidebarMenu',
components: { SidebarMenu }, // 递归组件注册
props: {
routes: { type: Array, required: true },
basePath: { type: String, default: '' },
isCollapsed: { type: Boolean, default: false }
},
methods: {
resolvePath(path) {
return this.basePath ? `${this.basePath}/${path}` : path;
},
handleClick(route) {
// 菜单点击事件处理
this.$router.push(route.path);
}
},
computed: {
activeMenu() {
// 计算当前激活的菜单
const route = this.$route;
return route.path;
}
}
};
</script>
通过递归调用sidebar-menu组件,无论菜单层级有多深,都能被正确渲染为嵌套的下拉菜单。
四、按钮级权限控制:指令化实现方案
诺依通过自定义 Vue 指令v-permission实现按钮级别的权限控制,核心逻辑是 "权限校验 + DOM 操作":
- 权限指令的实现
javascript
// src/directive/permission/index.js
import Vue from 'vue';
import { hasPermission } from '@/utils/permission';
// 注册v-permission指令
Vue.directive('permission', {
inserted(el, binding, vnode) {
const { value } = binding;
// 权限标识可以是字符串或数组
if (value && value instanceof Array && value.length > 0) {
const hasAuth = hasPermission(value);
if (!hasAuth) {
// 无权限则移除元素
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error('v-permission指令需要传入权限标识数组,如v-permission="[\'system:user:add\']"');
}
}
});
// 权限校验工具函数
// src/utils/permission.js
export function hasPermission(permissions) {
// 从Vuex获取用户拥有的权限集合
const userPermissions = store.getters && store.getters.permissions;
// 判断用户是否拥有目标权限(支持多权限"或"关系)
return permissions.some(permission => userPermissions.includes(permission));
}
- 指令在页面中的使用
在业务组件中,只需为按钮添加v-permission指令并传入所需权限标识,即可实现权限控制:
javascript
<!-- src/views/system/user/index.vue -->
<template>
<el-button
type="primary"
@click="handleAdd"
v-permission="['system:user:add']"
>
<i class="el-icon-plus"></i> 新增用户
</el-button>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button
size="mini"
@click="handleEdit(scope.row)"
v-permission="['system:user:edit']"
>
编辑
</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
v-permission="['system:user:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</template>
当用户无对应权限时,按钮会在组件初始化阶段被从 DOM 中移除,实现 "无权限则不可见" 的效果。
五、前端权限优化:缓存与动态更新
在实际应用中,权限可能会在用户使用过程中发生变更(如管理员修改了角色权限)。诺依通过以下方式优化权限动态性:
- 权限缓存策略
- 基础权限数据(角色、权限标识)存储在 Vuex 中,同时备份到sessionStorage,避免页面刷新后权限丢失;
- 动态路由信息仅存储在 Vue Router 实例中,刷新页面后会重新调用generateRoutes接口生成。
- 权限动态更新
当权限发生变更时,前端可通过调用reloadPermission方法强制刷新权限:
javascript
// 权限刷新方法
export function reloadPermission() {
// 清除Vuex中的权限数据
store.dispatch('user/resetInfo');
// 重新加载权限信息
store.dispatch('user/getInfo').then(() => {
// 重新生成路由
store.dispatch('permission/generateRoutes').then(routes => {
// 重置路由并注入新路由
router.matcher = new VueRouter({ mode: 'history' }).matcher;
router.addRoutes(routes);
// 刷新当前页面
router.go(0);
});
});
}
这种方式虽然会导致页面刷新,但能确保权限变更立即生效,适合权限调整不频繁的场景。
总结:前端权限的核心设计思想
诺依前端权限管理的实现,本质是 **"数据驱动的 UI 适配"**:通过后端返回的权限数据,前端动态调整路由配置、菜单结构和按钮显隐,同时通过路由守卫和后端校验构建双重安全机制。
其设计亮点在于:
- 指令化控制:将权限判断逻辑封装为指令,降低业务代码与权限逻辑的耦合;
- 递归组件:通过递归实现任意层级菜单的渲染,适应复杂的权限结构;
- 懒加载策略:路由组件和权限数据按需加载,平衡性能与安全性。
对于开发者而言,理解这些实现细节不仅能快速定位权限相关问题,更能在自定义权限系统时,借鉴其 "前端做展示控制,后端做安全校验" 的设计原则,构建既灵活又安全的权限体系。