RBAC 权限控制 - 前端
获取基本信息
①登录后,②获取用户信息(角色、部门)、token,③获取菜单信息。
①登录 post http://codercba.com:5000/login
获取登录相应信息和 token:
{
"id": 1,
"name": "coderwhy",
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6ImNvZGVyd2h5Iiwicm9sZSI6eyJpZCI6MSwibmFtZSI6Iui2hee6p-euoeeQhuWRmCJ9LCJpYXQiOjE3Mzxxxxxxxxx"
}
②get http://codercba.com:5000/users/1
获取用户信息(角色、部门等)(携带 token):
{
"id": 1,
"name": "coderwhy",
"realname": "coderwhy",
"cellphone": 18812345678,
"enable": 1,
"createAt": "2021-01-02T10:20:26.000Z",
"updateAt": "2021-01-03T04:50:13.000Z",
"role": {
"id": 1,
"name": "超级管理员",
"intro": "所有权限",
"createAt": "2021-01-02T10:01:52.000Z",
"updateAt": "2021-01-02T10:01:52.000Z"
},
"department": {
"id": 1,
"name": "总裁办",
"parentId": null,
"createAt": "2021-01-02T10:03:09.000Z",
"updateAt": "2021-01-05T08:25:46.000Z",
"leader": "coderwhy"
}
}
③菜单信息:根据角色获取对应的菜单树。发送 GET 请求(携带 token):http://codercba.com:5000/role/1/menu
。
[
{
"id": 38,
"name": "系统总览",
"type": 1,
"url": "/main/analysis",
"icon": "el-icon-monitor",
"sort": 1,
"children": [
{
"id": 39,
"url": "/main/analysis/overview",
"name": "核心技术",
"sort": 106,
"type": 2,
"children": null,
"parentId": 38
},
{
"id": 40,
"url": "/main/analysis/dashboard",
"name": "商品统计",
"sort": 107,
"type": 2,
"children": null,
"parentId": 38
}
]
},
{
"id": 1,
"name": "系统管理",
"type": 1,
"url": "/main/system",
"icon": "el-icon-setting",
"sort": 2,
"children": [
{
"id": 2,
"url": "/main/system/user",
"name": "用户管理",
"sort": 100,
"type": 2,
"children": [
{
"id": 5,
"url": null,
"name": "创建用户",
"sort": null,
"type": 3,
"parentId": 2,
"permission": "system:users:create"
},
{
"id": 6,
"url": null,
"name": "删除用户",
"sort": null,
"type": 3,
"parentId": 2,
"permission": "system:users:delete"
},
{
"id": 7,
"url": null,
"name": "修改用户",
"sort": null,
"type": 3,
"parentId": 2,
"permission": "system:users:update"
},
{
"id": 8,
"url": null,
"name": "查询用户",
"sort": null,
"type": 3,
"parentId": 2,
"permission": "system:users:query"
}
],
"parentId": 1
},
{
"id": 3,
"url": "/main/system/department",
"name": "部门管理",
"sort": 101,
"type": 2,
"children": [
{
"id": 17,
"url": null,
"name": "创建部门",
"sort": null,
"type": 3,
"parentId": 3,
"permission": "system:department:create"
},
{
"id": 18,
"url": null,
"name": "删除部门",
"sort": null,
"type": 3,
"parentId": 3,
"permission": "system:department:delete"
},
{
"id": 19,
"url": null,
"name": "修改部门",
"sort": null,
"type": 3,
"parentId": 3,
"permission": "system:department:update"
},
{
"id": 20,
"url": null,
"name": "查询部门",
"sort": null,
"type": 3,
"parentId": 3,
"permission": "system:department:query"
}
],
"parentId": 1
},
{
"id": 4,
"url": "/main/system/menu",
"name": "菜单管理",
"sort": 103,
"type": 2,
"children": [
{
"id": 21,
"url": null,
"name": "创建菜单",
"sort": null,
"type": 3,
"parentId": 4,
"permission": "system:menu:create"
},
{
"id": 22,
"url": null,
"name": "删除菜单",
"sort": null,
"type": 3,
"parentId": 4,
"permission": "system:menu:delete"
},
{
"id": 23,
"url": null,
"name": "修改菜单",
"sort": null,
"type": 3,
"parentId": 4,
"permission": "system:menu:update"
},
{
"id": 24,
"url": null,
"name": "查询菜单",
"sort": null,
"type": 3,
"parentId": 4,
"permission": "system:menu:query"
}
],
"parentId": 1
},
{
"id": 25,
"url": "/main/system/role",
"name": "角色管理",
"sort": 102,
"type": 2,
"children": [
{
"id": 26,
"url": null,
"name": "创建角色",
"sort": null,
"type": 3,
"parentId": 25,
"permission": "system:role:create"
},
{
"id": 27,
"url": null,
"name": "删除角色",
"sort": null,
"type": 3,
"parentId": 25,
"permission": "system:role:delete"
},
{
"id": 28,
"url": null,
"name": "修改角色",
"sort": null,
"type": 3,
"parentId": 25,
"permission": "system:role:update"
},
{
"id": 29,
"url": null,
"name": "查询角色",
"sort": null,
"type": 3,
"parentId": 25,
"permission": "system:role:query"
}
],
"parentId": 1
}
]
},
{
"id": 9,
"name": "商品中心",
"type": 1,
"url": "/main/product",
"icon": "el-icon-goods",
"sort": 3,
"children": [
{
"id": 15,
"url": "/main/product/category",
"name": "商品类别",
"sort": 104,
"type": 2,
"children": [
{
"id": 30,
"url": null,
"name": "创建类别",
"sort": null,
"type": 3,
"parentId": 15,
"permission": "system:category:create"
},
{
"id": 31,
"url": null,
"name": "删除类别",
"sort": null,
"type": 3,
"parentId": 15,
"permission": "system:category:delete"
},
{
"id": 32,
"url": null,
"name": "修改类别",
"sort": null,
"type": 3,
"parentId": 15,
"permission": "system:category:update"
},
{
"id": 33,
"url": null,
"name": "查询类别",
"sort": null,
"type": 3,
"parentId": 15,
"permission": "system:category:query"
}
],
"parentId": 9
},
{
"id": 16,
"url": "/main/product/goods",
"name": "商品信息",
"sort": 105,
"type": 2,
"children": [
{
"id": 34,
"url": null,
"name": "创建商品",
"sort": null,
"type": 3,
"parentId": 16,
"permission": "system:goods:create"
},
{
"id": 35,
"url": null,
"name": "删除商品",
"sort": null,
"type": 3,
"parentId": 16,
"permission": "system:goods:delete"
},
{
"id": 36,
"url": null,
"name": "修改商品",
"sort": null,
"type": 3,
"parentId": 16,
"permission": "system:goods:update"
},
{
"id": 37,
"url": null,
"name": "查询商品",
"sort": null,
"type": 3,
"parentId": 16,
"permission": "system:goods:query"
}
],
"parentId": 9
}
]
},
{
"id": 41,
"name": "随便聊聊",
"type": 1,
"url": "/main/story",
"icon": "el-icon-chat-line-round",
"sort": 4,
"children": [
{
"id": 42,
"url": "/main/story/chat",
"name": "你的故事",
"sort": 108,
"type": 2,
"children": null,
"parentId": 41
},
{
"id": 43,
"url": "/main/story/list",
"name": "故事列表",
"sort": 109,
"type": 2,
"children": [],
"parentId": 41
}
]
}
]
用户登录提交 action:
async userLoginAction(loginData: ILoginData) {
// 获取用户登录返回信息
const res = await userLogin(loginData);
const id = res.data.id;
// 处理 token
this.token = res.data.token;
localCache.setCache(LOGIN_TOKEN, res.data.token);
// 获取用户信息
const userInfoResult = await getUserInfoById(id);
this.userInfo = userInfoResult.data;
localCache.setCache(USER_INFO, userInfoResult.data);
// 获取权限
const userMenusResult = await getUserMenusByRoleId(id);
this.userMenus = userMenusResult.data;
localCache.setCache(USER_MENUS, userMenusResult.data);
// 动态添加路由
const routes = mapMenus(this.userMenus);
routes.forEach((route) => router.addRoute("main", route));
// 路由跳转到首页
router.push("/");
},
根据菜单动态注册路由
动态路由:根据用户的权限信息,动态的添加路由(而不是一次性注册所有路由)
- 基于角色(Role)的动态路由管理
const roles = {
"superadmin":[所有的路由],=>router.main.children
"admin":[一部分路由l,=>router.main.children
"service":[少部分路由],=>router.main.children,
"manager":[后面新添加的role]=>重新发布/后端返回这个对象(Gson数据,后端必须组织好这个json)
}
弊端:每增加一个角色,都要增加key/value
- 基于菜单(menu)的动态路由管理
userMenus:=>动态展示菜单(系统总览/核心技术/用户管理/角色管理/)
然后将菜单映射成路由对象。
原本的路由:
import { createRouter, createWebHistory } from "vue-router";
import { localCache } from "@/utils/cache";
import { LOGIN_TOKEN } from "@/constants";
import {firstMenu} from "@/utils/mapMenus";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
redirect: "/main"
},
{
path: "/main",
name: "main",
component: () => import("@/views/main/MainView.vue")
},
{
path: "/login",
name: "login",
component: () => import("@/views/login/LoginView.vue")
},
{
path: "/:pathMatch(.*)",
name: "NotFound",
component: () => import("@/views/not-found/NotFound.vue")
}
]
});
router.beforeEach((to, from, next) => {
if (to.name !== "login" && !localCache.getCache(LOGIN_TOKEN)) next({ name: "login" });
if (to.path === '/main') next({ path: firstMenu?.url });
next();
});
export default router;
然后将动态路由单独划分一个路由文件夹:
dashboard.ts
export default {
path: '/main/analysis/dashboard',
name: 'dashboard',
component: () => import('@/views/main/analysis/dashboard/dashboard.vue'),
children: []
}
刚好和页面目录结构保持一致:
然后就可以在登录后触发 action 执行添加路由的操作(见上userLoginAction)
// 动态添加路由
const routes = mapMenus(this.userMenus);
routes.forEach((route) => router.addRoute("main", route));
映射菜单到路由的工具:
import type { RouteRecordRaw } from "vue-router";
export const loadLocalRoutes = () => {
// 动态添加路由
const localRoutes: RouteRecordRaw[] = [];
// 获取本地路由文件
const files: Record<string, any> = import.meta.glob("@/router/main/**/*.ts", {
eager: true /*立刻获取文件信息,而不是懒加载获取*/
});
for (const path in files) {
const module = files[path];
localRoutes.push(module.default);
}
return localRoutes;
};
export let firstMenu: any = null;
/**
* 将用户菜单转换为路由
*/
export const mapMenus = (userMenus: any[]) => {
const localRoutes = loadLocalRoutes();
const routes: RouteRecordRaw[] = [];
// 根据菜单信息匹配该用户权限下的路由
for (const menuItem of userMenus) {
if (menuItem.children) {
for (const submenu of menuItem.children) {
const route = localRoutes.find((route) => route.path === submenu.url);
if (route) {
// 给父级菜单添加重定向
if (!routes.find((r) => r.path === menuItem.url)) {
routes.push({ path: menuItem.url, redirect: route.path });
}
// 添加二级菜单
routes.push(route);
}
if (!firstMenu && route) {
firstMenu = submenu;
// console.log("=>(mapMenus.ts:35) firstMenu", firstMenu);
}
}
}
}
return routes;
};
但是像上面这样做有个问题,刷新页面路由会丢失,因此我们写一个方法使动态路由持久化。
loadAsyncRoutes() {
// 用户刷新 动态获取路由 防止因为刷新导致缓存的动态路由丢失
if (this.token && this.userMenus && this.userInfo) {
const routes = mapMenus(this.userMenus);
routes.forEach((route) => {
router.addRoute("main", route);
});
}
}
然后在入口文件main.ts中使用。
const registerStore = (app: App<Element>) => {
app.use(pinia);
const loginStore = useLoginStore();
// todo 重新登录 如果 main 路由中已经含有子路由,则不执行动态添加路由
loginStore.loadAsyncRoutes();
};
export default registerStore;
const app = createApp(App);
app.use(registerStore);