引言
在管理系统中,随着用户角色的多样化和业务流程的复杂化,前端权限控制的重要性愈发凸显。它基于后端返回的权限数据,对界面展示和操作进行精准限制,成为提升用户体验、提高管理效率以及保障系统安全的关键手段。在不同的项目中,权限管理的复杂性以及用户角色的多样性各不相同。其核心需求主要集中在:
- 菜单动态显示:根据用户角色和权限动态展示菜单项;
- 页面访问控制:通过前端路由守卫防止未授权访问;
- 操作级权限管理:根据用户权限决定页面操作按钮、区域的显示与功能限制。
在实际项目中,我全程参与了前端权限管理的实现,过程中不仅解决了诸多实际问题,还探索出一些独特的解决方案。本文旨在记录和分享我在项目中对权限管理控制的实践经验与心得体会。
权限管理控制实现
请求后端获取权限数据
1. 项目角色固定,权限不频繁改动
项目的角色是固定的,角色权限在项目初期已经设定好,不频繁改动,没有动态配置权限的需求,那么请求权限数据时只需要返回当前用户的角色即可。前端在进行权限判断时,只需根据用户角色进行判断。
2. 项目角色不固定,权限支持频繁修改
项目的角色不固定,支持增删改,权限支持动态修改,权限配置页面允许管理员随时调整权限,那么请求权限数据时不仅要返回当前用户的角色,还需要返回当前用户所属角色的并集的权限列表。权限列表的数据结构取决于权限之间是否存在层级关系,存在层级关系时为树状结构,否则为平铺结构。
临时缓存权限数据
为了提高系统的性能和响应速度,权限数据通常会被临时缓存到前端的存储中,如 sessionStorage
或 Pinia
。这样可以避免每次操作时都向后端请求权限数据。然而,如果项目支持权限的动态修改,还需要在特定时机更新缓存的权限数据,例如在切换路由时更新权限。
实现一个鉴权函数
鉴权函数是前端权限管理的核心,它根据缓存的权限数据判断用户是否有权限执行某个操作或访问某个资源。
- 实现鉴权函数:根据权限数据,判断用户是否有某个权限。
- 调用鉴权函数:在需要进行权限判断的地方调用鉴权函数。
ts
// 最简单实现
export function hasPermission(permission) {
const permissions = JSON.parse(localStorage.getItem('permissions'));
return permissions.includes(permission);
}
// 可以根据项目需求,修改鉴权函数做更复杂的权限判断(如:权限联合判断)
在合适的时机调用鉴权函数进行过滤和判断。
菜单动态展示和页面的访问控制
Q: 菜单数据和路由数据是否应该调用接口由后端返回?
A: 在实践中,我认为菜单数据和路由数据并不需要由后端返回。前后端只需要根据需求协商好权限列表结构,并定义好每个权限的唯一ID即可。在前后端分离的开发模式下,后端开发人员通常不关心前端菜单和路由的具体实现和展示方式。前后端的唯一交集点是权限列表,后端只需要根据当前用户返回其拥有的所有权限列表即可。至于菜单和路由的控制,应该由前端开发人员根据权限列表自行实现,这样可以减少前后端的耦合。
Q: 前端菜单数据应该怎么生成?
A: 我认为在项目中对于路由配置应该有一个明确的规范,然后根据路由表生成一份菜单数据。这种方式不仅可以确保菜单数据与路由表的一致性,还可以减少重复工作,提高开发效率。
规范化路由配置
ts
// 路由属性遵循 Vue Router 的标准用法,主要扩展配置都在 meta 数据中。
// 支持以下扩展功能:
// - hidden: 路由是否隐藏,不显示在菜单栏中
// - affix: 菜单是否固定在菜单栏中
// - roles: 允许访问该路由的角色列表
// - permission: 访问该路由所需的权限
// - title: 菜单或页面的标题
// - icon: 菜单图标
const routes = [
{
path: "/",
name: "Dashboard",
component: Dashboard,
// 路由元信息
meta: {
title: "仪表盘", // 菜单或页面的标题
icon: "dashboard", // 菜单图标
affix: true // 是否固定在菜单栏中
}
},
{
path: "/user",
name: "UserManagement",
component: UserManagement,
// 路由元信息
meta: {
title: "用户管理", // 菜单或页面的标题
icon: "user", // 菜单图标
roles: ["admin"], // 允许访问该路由的角色列表
permission: "user_manage" // 访问该路由所需的权限
},
// 嵌套路由
children: [
{
path: "list",
name: "UserList",
component: UserList,
// 路由元信息
meta: {
title: "用户列表", // 菜单或页面的标题
icon: "list", // 菜单图标
roles: ["admin"], // 允许访问该路由的角色列表
permission: "user_list" // 访问该路由所需的权限
}
},
{
path: "add",
name: "UserAdd",
component: UserAdd,
// 路由元信息
meta: {
title: "添加用户", // 菜单或页面的标题
icon: "add", // 菜单图标
roles: ["admin"], // 允许访问该路由的角色列表
permission: "user_add" // 访问该路由所需的权限
}
}
]
},
{
path: "/login",
name: "Login",
component: Login,
// 路由元信息
meta: {
hidden: false, // 路由是否隐藏,不显示在菜单栏中
isWhite: true // 是否为白名单路由(无需权限即可访问)
}
}
];
通过规范化路由配置,可以生成一份有结构的菜单数据。然后在 layout
页面中声明一个计算属性,根据路由生成的菜单数据和权限列表进行过滤判断,返回一个过滤后的菜单数据。这种实现方式不仅减少了前后端的耦合,还提高了开发效率和系统的可维护性。
Q: 页面的访问控制如何实现?
A: 动态路由和路由守卫
动态路由
- 定义基础路由 :在
route.js
中定义登录、404 和 Dashboard 等基础路由。 - 定义所有可能的路由 :新建一个文件(
dynamicRoutes.js
)中定义所有可能的路由。 - 过滤有权限的路由:根据用户权限过滤出用户有权限的路由。
- 动态添加路由 :使用
router.addRoute
动态添加这些路由。
路由守卫
在 route.js
中定义好所有可能的路由后
- 定义路由守卫 :在
route.js
中定义全局路由守卫。 - 判断权限 :根据
meta.isWhite
和meta.permission
等字段决定是否允许跳转。
ts
router.beforeEach((to, from, next) => {
const isWhite = to.meta.isWhite;
const userPermissions = JSON.parse(localStorage.getItem('permissions') || '[]');
if (isWhite) {
next(); // 白名单路由直接放行
} else if (!token) {
next('/login'); // 未登录用户重定向到登录页面
} else {
const requiredPermission = to.meta.permission;
// 如果有权限属性,就判断权限,有权限和无权限属性就正常跳转
if (requiredPermission && !userPermissions.includes(requiredPermission)) {
next('/404'); // 无权限跳转到404页面
} else {
next(); // 有权限正常跳转
}
}
});
操作级权限管理
-
函数级权限管理
函数级权限管理通常用于控制函数的执行。在函数执行前,通过 卫语句(Guard Clause) 检查用户是否有权限执行该函数。如果没有权限,可以抛出错误或返回一个友好的提示或直接
return
。ts// 删除用户 function deleteUser(userId) { if (!hasPermission('user_delete')) { Message('您没有删除用户的权限'); return } // 执行删除用户的逻辑 console.log(`删除用户: ${userId}`); }
-
组件级权限管理
组件级权限管理通常用于控制组件的显示或隐藏。例如,某些按钮、页面区域可能只有特定角色或权限的用户才能看到。
- 组件模板中使用条件渲染来实现。(基础用法)
html<template> <div> <button v-if="hasPermission('user_add')">添加用户</button> <button v-if="hasPermission('user_edit')">编辑用户</button> <button v-if="hasPermission('user_delete')">删除用户</button> </div> </template>
- 为了简化条件渲染组件,可以使用 Vue 的自定义指令来实现权限控制。自定义指令可以在模板中直接使用,提高代码的可读性和复用性。
tsimport { hasPermission } from './permissions'; export const vAuth = { mounted(el, binding) { const { value } = binding; if (!hasPermission(value)) { const parent = el.parentNode; if (parent) { parent.removeChild(el); } else { el.remove(); } } } };
html<template> <div> <button v-auth="'user_add'">添加用户</button> <button v-auth="'user_edit'">编辑用户</button> <button v-auth="'user_delete'">删除用户</button> </div> </template>
FAQ:
使用自定义指令进行权限控制虽然简单,但在处理复杂组件或生命周期方法时存在一些局限性。
- DOM 结构复杂时的问题
组件的 DOM 结构复杂,例如使用了
el-tab-pane
或其他封装的组件,直接移除 DOM 元素可能会导致页面显示不正确。例如,即使移除了某些 DOM 元素,页面可能仍然会显示部分占位内容,导致页面布局畸形。- 生命周期方法无法阻止
使用指令移除了 DOM 元素,组件的生命周期方法(如
mounted
)仍然会运行。这可能导致不必要的请求或其他业务逻辑执行,浪费资源并可能引起错误。解决方案: 使用条件渲染或封装高阶组件(HOC)
- 条件渲染方案 :使用
v-if
进行条件渲染,适用于简单场景。 - 封装高阶组件(HOC) :封装一个高阶组件来处理权限逻辑,适用于复杂场景,可以复用权限控制逻辑。
- 封装高阶组件(HOC)
html<script setup lang="ts"> import { computed } from "vue"; export interface AuthContainerProps { permission?: string | string[]; } defineOptions({ name: "AuthContainer", }); const props = defineProps<AuthContainerProps>(); const isAuthorized = computed(() => { // 如果没有设置权限,则默认显示 if (!props.permission) return true; return hasPermission(props.permission); }); </script> <template> <slot v-if="isAuthorized"></slot> </template>
组件使用
html<template> <AuthContainer permission="user_add"> <button>添加用户</button> </AuthContainer> </template>
- 对循环渲染的组件进行权限控制,还可以先通过计算属性过滤出有权限显示的数据,然后再进行循环渲染。这种方式可以避免在模板中直接进行复杂的逻辑判断,使代码更加清晰和高效
后记
鉴权函数是前端权限管理的核心,开发者需根据项目需求设计高效、准确的鉴权逻辑,同时保证其灵活性和扩展性,以应对复杂情况.
本文章提供了一个前端权限管理实现的思路,内容较为浅显。对于缓存数据、更新数据、前端是否根据后端数据操作生成菜单和路由表,以及组件级权限控制的具体实现方式,都需要根据具体的项目需求灵活处理。
感谢阅读,敬请斧正!