前言
在写后台管理或者类似的项目中,开发者希望用户只能访问部分路由或者路由中的部分按钮 那么又该如何实现呢 本篇是基于对硅谷甄选项目(可搜到)的代码进行一个总结
菜单权限
方案一:后端动态返回的路由配置
路由的拆分
- 静态路由(常量路由):大家都可以拥有的路由
- 异步路由 :不同身份有的有这个路由 有的没有
- 任意路由 : 比如说用户输入其他路由都跳到404
后端返回路由配置:后端根据用户的不同角色返回不同的路由表(这个在一开始的时候前后端就应该商量好路由的命名,后端的命名要和前端路由的name保持一致) 前端接收路由配置 :通过API调用后端的接口,获取路由配置 前段生成路由 :前段通过vue router 动态添加路由,将后端返回的路由表注入到vue-router中 渲染页面 :前端根据动态添加的路由,展示用户可以访问的页面
后端返回的数据:
下面对路由进行拆分 分为常量 异步 任意
//对外暴露配置路由(常量路由)
export const constantRoute = [
{
//登录路由
path: "/login",
component: () => import("@/views/login/index.vue"),
name: "login", //命名路由
meta: {
title: "登录", //菜单标题
hidden: true, //路由的标题在菜单中是否隐藏
},
},
{
//登录成功以后展示数据的路由
path: "/",
component: () => import("@/layout/index.vue"),
name: "layout",
meta: {
hidden: false,
},
redirect: "/home",
children: [
{
path: "/home",
component: () => import("@/views/home/index.vue"),
meta: {
title: "首页",
hidden: false,
icon: "HomeFilled",
},
},
],
},
{
path: "/404",
component: () => import("@/views/404/index.vue"),
name: "404",
meta: {
title: "404",
hidden: true,
},
},
{
path: "/screen",
component: () => import("@/views/screen/index.vue"),
name: "Screen",
meta: {
hidden: false,
title: "数据大屏",
icon: "Platform",
},
},
];
//异步路由
export const asnycRoute = [
{
path: "/acl",
component: () => import("@/layout/index.vue"),
name: "Acl",
meta: {
hidden: false,
title: "权限管理",
icon: "Lock",
},
redirect: "/acl/user",
children: [
{
path: "/acl/user",
component: () => import("@/views/acl/user/index.vue"),
name: "User",
meta: {
hidden: false,
title: "用户管理",
icon: "User",
},
},
{
path: "/acl/role",
component: () => import("@/views/acl/role/index.vue"),
name: "Role",
meta: {
hidden: false,
title: "角色管理",
icon: "UserFilled",
},
},
{
path: "/acl/permission",
component: () => import("@/views/acl/permission/index.vue"),
name: "Permission",
meta: {
hidden: false,
title: "菜单管理",
icon: "Monitor",
},
},
],
},
{
path: "/product",
component: () => import("@/layout/index.vue"),
name: "Product",
meta: {
hidden: false,
title: "商品管理",
icon: "Goods",
},
redirect: "/product/trademark",
children: [
{
path: "/product/trademark",
component: () => import("@/views/product/trademark/index.vue"),
name: "Trademark",
meta: {
hidden: false,
title: "品牌管理",
icon: "ShoppingCartFull",
},
},
{
path: "/product/attr",
component: () => import("@/views/product/attr/index.vue"),
name: "Attr",
meta: {
hidden: false,
title: "属性管理",
icon: "CollectionTag",
},
},
{
path: "/product/spu",
component: () => import("@/views/product/spu/index.vue"),
name: "Spu",
meta: {
hidden: false,
title: "SPU管理",
icon: "Calendar",
},
},
{
path: "/product/sku",
component: () => import("@/views/product/sku/index.vue"),
name: "Sku",
meta: {
hidden: false,
title: "SKU管理",
icon: "Orange",
},
},
],
},
];
//任意路由
//任意路由
export const anyRoute = {
//任意路由
path: "/:pathMatch(.*)*",
redirect: "/404",
name: "Any",
meta: {
title: "任意路由",
hidden: true,
icon: "DataLine",
},
};获取路由的方法
注意:拼接数组时,任意路由要放在最后
//硅谷333: routes['Product','Trademark','Sku']
let guigu333 = ["Product", "Trademark", "Sku"];
function filterAsyncRoute(asnycRoute, routes) {
return asnycRoute.filter((item) => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
item.children = filterAsyncRoute(item.children, routes);
}
return true;
}
});
}
//硅谷333需要展示的异步路由
let guigu333Result = filterAsyncRoute(asnycRoute, guigu333);
//将常量 异步 任意合并
console.log([...constRoute, ...guigu333Result, anyRoute], "硅谷333");Product为一级 TradeMark Sku Spu为Product的二级
如果不使用递归 只能筛选出一级 当筛选出Product时,由于三者均为二级路由 并不会再进行深层过滤,最后渲染到页面上三者都在 如果要只展示某几个 要用到递归进行深层筛选并重新赋值给item.children
获取路由完整方法
。。。。。。
import router from '@/router'
//引入路由(常量路由)
import { constantRoute, asnycRoute, anyRoute } from '@/router/routes'
//用于过滤当前用户需要展示的异步路由
function filterAsyncRoute(asnycRoute: any, routes: any) {
return asnycRoute.filter((item: any) => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
//硅谷333账号:product\trademark\attr\sku
item.children = filterAsyncRoute(item.children, routes)
}
return true
}
})
}
//创建用户小仓库
const useUserStore = defineStore('User', {
//小仓库存储数据地方
state: (): UserState => {
return {
。。。。。。。
menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
us。。。。。。
}
},
//处理异步|逻辑地方
actions: {
。。。。。。。
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
//计算当前用户需要展示的异步路由
const userAsyncRoute = filterAsyncRoute(asnycRoute, result.data.routes)
//菜单需要的数据整理完毕
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute];
//目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
。。。。。。
})
//对外暴露小仓库
export default useUserStore过滤出来每个用户所需要的异步路由(userAsnycRoute) 然后赋值给仓库的menuRoutes 由于这只是将menuRoutes更新 也就是说侧边栏的数据有了 但实际上项目的路由还是只有常量路由,需要动态添加
残留问题
1.深拷贝
之前获取需要的路由方法是浅拷贝,会改变原有的路由 比如说:我现在登录了一个超级权限管理员的账号拥有所有路由 然后又登录了一个只有部分权限(guigu333)的账号 此时经过过滤,menuRoutes只有部分路由展示 然后再次登录超级权限管理员的,就会发现超级权限管理员展示的路由不是全部的路由,而是上次guigu33筛选出的路由 这是因为 : 1.两次登录没有刷新页面 数据也不会进行重新刷新 2.数组是进行的浅拷贝,会改变原有的路由,也就是说asnycRoute被修改了
这里引用深拷贝的方法lodash 需要npm install 进行下载
//引入深拷贝方法
//@ts-expect-error
import cloneDeep from 'lodash/cloneDeep'
。。。。。。
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
//计算当前用户需要展示的异步路由
const userAsyncRoute = filterAsyncRoute(
cloneDeep(asnycRoute),
result.data.routes,
)
//菜单需要的数据整理完毕
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
//目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
;[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},2.路由加载问题
这样配置路由后,如果你访问的是异步路由,会在刷新的时候出现空白页面。原因是异步路由是异步获取的,加载的时候还没有。因此我们可以在路由守卫文件中改写。这个的意思就是一直加载。
在这里也分享一下路由守卫的代码(其中有进度条)
// 路由鉴权 项目当中路由能不能被访问的权限校验 (某一个路由什么条件下可以访问)
import router from "@/router";
import setting from "./setting";
import nprogress from "nprogress";
// 引入进度条样式
import "nprogress/nprogress.css";
nprogress.configure({ showSpinner: false });
// 获取用户相关的小仓库内部token数据 去判断用户是否登录
import useUserStore from "@/store/modules/user";
import pinia from "./store";
let userStore = useUserStore(pinia);
// 全局守卫:项目当中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title}-${to.meta.title as string}`;
// to:将要访问那个路由
// from:从哪个路由来
// next 路由的放行函数
// 访问某一个路由之前的守卫
nprogress.start();
let token = userStore.token;
// 获取用户名字
let username = userStore.username;
// 用户登录判断
if (token) {
// 登录成功 访问login 不能访问 指向首页
if (to.path == "/login") {
next({ path: "/" });
} else {
// 登录成功访问其余6个
//有用户信息
if (username) {
next();
} else {
// 没有用户信息 就在守卫这里发请求获取到了用户信息再放行
try {
// 获取用户信息以后再进行放行
await userStore.userInfo();
next({ ...to });
} catch (error) {
// token过期
// 用户手动修改了本地token
// 那么采用退出登录->清空数据
// 万一:刷新的时候是异步路由,有可能获取到用户信息,异步路由还没有加载完毕,出现空白的效果
await userStore.userLogout();
next({ path: "./login", query: { redirect: to.path } });
}
}
}
} else {
// 用户未登录判断
if (to.path == "/login") {
next();
} else {
next({ path: "./login", query: { redirect: to.path } });
}
}
next();
});
// 全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done();
});
// 问题一:任意路由切换实现进度条业务 ---nprogress
// 问题二: 路由鉴权(路由组件访问权限的设置)
// 全部的路由组件 登录 404 任意 首页 数据大屏 权限管理(三个子路由) 商品管理 (4个子路由)
// 用户未登录 可以访问login 其余6个路由不可以访问
// 用户登录 不可以访问login(指向首页) 其余的路由可以访问方案二:前端基于角色的路由表
前端配置好完整的路由表,并通过meta对象中设置的roles属性来指定那些角色可以访问某些路由 通过vue-router的导航守卫beforeEach()来权限校验,拦截不符合访问权限的访问请求
jsx;
复制代码;
const routerMap = [
{
path: "/permission",
component: Layout,
meta: {
title: "permission",
roles: ["admin", "editor"], // 允许的角色
},
children: [
{
path: "page",
component: () => import("@/views/permission/page"),
meta: {
title: "pagePermission",
roles: ["admin"], // 只有管理员可访问
},
},
],
},
];
router.beforeEach((to, from, next) => {
const userRole = localStorage.getItem("userRole");
if (to.meta.roles && !to.meta.roles.includes(userRole)) {
// 如果用户角色不在路由的 roles 中,跳转到无权限页面
next("/unauthorized");
} else {
// 否则正常跳转
next();
}
});按钮权限
在某些场景下,除了页面级别的权限控制,按钮级别的控制也是常见的需求,通过自定义指令,可以根据用户的权限动态控制按钮的显示与隐藏
步骤: 1.创建一个src/directive/has.ts文件
import pinia from "@/store";
import useUserStore from "@/store/modules/user";
let userStore = useUserStore(pinia);
export const isHasButton = (app: any) => {
// 获取对应的用户仓库
// 全局自定义指令
app.directive("has", {
// 代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次
// 指令的定义
mounted(el: any, options: any) {
// 自定义指令右侧的数值,如果在用户信息buttons数组当中没有
//从DOM树上干掉
if (!userStore.buttons.includes(options.value)) {
el.parentNode.removeChild(el);
}
},
});
};2.在main.ts文件中注入该文件
// 引入仓库
import pinia from "@/store";
const app = createApp(App);
// 安装element-plus插件
app.use(ElementPlus, {
locale: zhCn, //国际化配置
});
app.use(pinia);
// 安装自定义插件
app.use(globalComponent);
// 注册模版路由
app.use(router);
// 引入路由鉴权文件
import "./permission";
// 引入自定义指令文件
import { isHasButton } from "./directive/has";
isHasButton(app);
// 将应用挂载到挂载点上
app.mount("#app");总结
前端权限控制不仅仅是页面和路由的访问权限,还包括按钮、操作权限的细粒度控制。它与后端的权限体系相辅相成,前端负责用户体验的优化与增强,而后端则负责核心的权限认证与校验。在实际项目中,合理设计权限系统可以提高系统的安全性、灵活性和可维护性。通过接口、路由和按钮的权限控制机制,前端开发者可以构建一个更加安全、用户友好的应用系统。
