原文来源于:程序员成长指北;作者:苏苏同学
如有侵权,联系删除
前言
大家好,我是考拉🐨。分享一篇好文~
简介
哈喽大家好,我是苏苏同学,前面我们聊到了Vue
和React
的路由拦截鉴权,有很多同学跟我反馈说太简单了,只能判断有没有登录,对于有用户角色权限的系统就不适用了。所以我们今天再深入聊一下Vue
和React
路由的角色权限。
角色权限
角色权限,简单来说就是登录的用户能看到系统的哪些页面,不能看到系统的哪些页面。一般是后台管理系统才会涉及到如此复杂的角色权限。
对于 vue
技术栈,实现角色权限一般有两种方式。
第一种是利用 beforeEach
全局前置守卫。
第二种是利用 addRoutes
方法。
我们先来看看第一种
通过 beforeEach 实时监测用户权限
这种方案的核心就是首先在 routes
里面事先定义好路由的权限,然后在 beforeEach
全局前置守卫里面进行权限逻辑判断。看用户所拥有的角色和我们配置在路由里面的 roles
是否相匹配。匹配则允许进入,不匹配则重定向到无权限提示页面。
定义 routes
首先我们定义好系统的路由,对于非首页,我们一般都会使用路由懒加载。
在 meta
里面可以定义我们需要的元数据,这里需要加上我们路由的角色roles
。也就是说进入该路由用户所需要具备的角色权限。如果没定义则代表任意角色都能进入。
js
复制代码
// router/routes.js
import Home from "../views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
meta: {
needLogin: false, // 不需要登录
title: "首页",
},
},
{
path: "/about",
name: "About",
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"), // 路由懒加载
meta: {
needLogin: true, // 需要登录
title: "关于",
roles: ["admin", "manage"], // 该页面只有admin和普通管理员才能进入
},
},
{
path: "/nopermission", // 没权限就进入该页面
name: "NoPermission",
component: () =>
import(
/* webpackChunkName: "nopermission" */ "../views/NoPermission.vue"
), // 路由懒加载
meta: {
needLogin: true, // 需要登录
title: "暂无权限",
},
},
{
path: "/userlist",
name: "UserList",
component: () =>
import(/* webpackChunkName: "userlist" */ "../views/UserList.vue"), // 路由懒加载
meta: {
needLogin: true, // 需要登录
title: "用户管理",
roles: ["admin"], // 该页面只有admin才能进入
},
},
{
path: "/login",
name: "Login",
component: () =>
import(/* webpackChunkName: "login" */ "../views/Login.vue"), // 路由懒加载
meta: {
needLogin: false, // 不需要登录
title: "登录",
},
},
];
export default routes;
实例化 Router
然后创建路由,vue2
和vue3
创建路由的时候稍有区别,但是在路由鉴权那块是通用的。
js
复制代码
// router/index.js
// vue2 写法
import VueRouter from "vue-router";
import routes from "./routes"
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
// vue3 写法
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes"
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
定义路由拦截鉴权逻辑
创建好路由后,我们可以来定义路由拦截的逻辑了,主要通过 beforeEach
全局前置守卫。因为只要页面发生跳转都会进入 beforeEach
全局前置守卫。
这里的核心逻辑就是判断前往的页面是否需要登录,需要登录就进一步判断当前系统是否有token,没有token则重定向到登录页
如果有token,则进一步判断是否有用户信息,如果没有用户信息就获取用户信息
有了用户信息后再判断进入页面需要的角色是否和用户信息里面的角色相匹配,匹配则进入页面,不匹配则进入系统的无权限提示页面。
js
复制代码
// router/index.js
// vue2和vue3通用
router.beforeEach(async (to, from, next) => {
// 如果需要登录
if (to.meta.needLogin) {
// 获取token
const token = localStorage.getItem("token");
// 如果有token 则直接放行
if (token) {
// 获取用户信息,从store里面获取
let userInfo = store.getters["getUserInfo"];
// 如果没有用户信息就获取用户信息
if (!userInfo) {
userInfo = await store.dispatch("getUserInfoAction");
}
// 如果页面需要权限,并且用户角色不满足则去无权限提示页
if (to.meta.roles && !to.meta.roles.includes(userInfo.role)) {
return next("/nopermission");
}
next();
} else {
// 否则去登录页
next("/login");
}
} else {
// 不需要登录则直接放行
next();
}
});
// 修改标题的工作可以放在全局后置守卫
router.afterEach((to, from) => {
if (to.meta.title) {
document.title = to.meta.title;
}
});
我们再来看看 store
的逻辑
js
复制代码
import { createStore } from "vuex";
export default createStore({
state: {
userInfo: null,
},
getters: {
getUserInfo: (state) => state.userInfo,
},
mutations: {
setUserInfo(state, payload) {
state.userInfo = payload;
},
},
actions: {
async getUserInfoAction({ commit }) {
// 模拟后端获取用户信息的api
const getUserInfoApi = () => {
return Promise.resolve({ role: "manage", name: "jack" }); // 假设角色为 manage
};
const userInfo = await getUserInfoApi();
commit("setUserInfo", userInfo);
return userInfo;
},
},
});
使用
创建完路由后需要在 main.js
导入使用
js
复制代码
import router from "./router";
// vue2写法
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
// vue3写法
const app = createApp(App);
app.use(router).mount("#app");
在没登录的情况下,去about
和userlist
页面都会重定向到登录页。
给本地添加token
模拟登录完成后,因为用户信息角色是 manage
,所以去 about
是没问题的,去 userlist
则会重定向到没有权限页面。
总结
优点:
实现简单,路由的权限在 meta
的 roles
里面配置。用户信息里面只需要包含当前用户的角色。
缺点:
系统所拥有的角色必须事先知道,然后以死代码的形式配置在 routes
里面,不支持动态添加角色。适用于小型角色固定的系统。
通过 addRoutes 动态添加路由
这种方案的核心就是调用后端接口,返回当前用户角色所拥有的菜单,格式化成路由后通过 addRoutes
将这些路由动态添加到系统。
定义 routes
因为路由从后端获取,所以这里我们只需要定义基本通用路由,也就是不涉及到权限的路由。比如登录页、没权限页、首页。
js
复制代码
import Home from "../views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
meta: {
title: "首页",
},
},
{
path: "/nopermission", // 没权限就进入该页面
name: "NoPermission",
component: () =>
import(
/* webpackChunkName: "nopermission" */ "../views/NoPermission.vue"
), // 路由懒加载
meta: {
title: "暂无权限",
},
},
{
path: "/login",
name: "Login",
component: () =>
import(/* webpackChunkName: "login" */ "../views/Login.vue"), // 路由懒加载
meta: {
title: "登录",
},
},
];
export default routes;
实例化 Router
然后创建路由,vue2
和vue3
创建路由的时候稍有区别。
js
复制代码
// router/index.js
// vue2 写法
import VueRouter from "vue-router";
import routes from "./routes"
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
// vue3 写法
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes"
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
定义路由拦截鉴权逻辑
创建好路由后,我们就可以定义动态添加路由的逻辑啦。
这里的核心逻辑就是判断前往的页面是否在白名单内,不在白名单就进一步判断当前系统是否有token,没有token则重定向到登录页
如果有token,则进一步判断是否有用户菜单信息,如果没有用户菜单信息就获取用户菜单信息
有了用户菜单信息后,就先将菜单信息转换成前端路由需要的格式,然后通过addRoutes方法将路由动态添加到系统。
当用户前往不存在的路由时,路由的 name 属性会是
undefined
,因此就重定向到无权限提示页面
js
复制代码
// router/index.js
// vite创建的项目 使用这种方法实现动态加载
const modules = import.meta.glob("../views/*.vue");
// 转换成 vue-router 需要的格式
const transformRoute = (menus) => {
return menus.map((menu) => {
return {
path: menu.path,
name: menu.name,
component: modules[menu.component],
meta: {
title: menu.title,
},
};
});
};
// vue3需要手动实现该方法
const addRoutes = (routes) => {
routes.forEach((route) => {
router.addRoute(route);
});
};
// 白名单页面,不需要权限
const whiteLists = ["/login", "/"];
router.beforeEach(async (to, from, next) => {
// 是否是白名单
if (!whiteLists.includes(to.path)) {
// 获取token
const token = localStorage.getItem("token");
// 如果有token
if (token) {
// 获取用户菜单信息,从store里面获取
let userMenus = store.getters["getUserMenus"];
// 如果没有用户菜单信息就获取用户菜单信息
if (!userMenus) {
userMenus = await store.dispatch("getUserMenuAction");
// 菜单转成路由
const userRoute = transformRoute(userMenus);
// 动态添加路由 vue2 和 vue3 有细微差别
// vue2
// router.addRoutes(userRoute)
// vue3
// 因为 vue3 移除了 router.addRoutes()方法,所以需要手动实现addRoutes方法。
addRoutes(userRoute);
return next({ ...to });
}
// 有name说明路由存在,否则说明没有该路由
if (to.name) {
next();
} else {
// 去无权限页面
next("/nopermission");
}
} else {
// 否则去登录页
next("/login");
}
} else {
// 是白名单则直接进入
next();
}
});
// 修改标题的工作可以放在全局后置守卫
router.afterEach((to, from) => {
if (to.meta.title) {
document.title = to.meta.title;
}
});
我们再来看看 store
的逻辑
js
复制代码
import { createStore } from "vuex";
export default createStore({
state: {
userMenus: null,
},
getters: {
getUserMenus: (state) => state.userMenus,
},
mutations: {
setUserMenus(state, payload) {
state.userMenus = payload;
},
},
actions: {
async getUserMenuAction({ commit }) {
// 模拟后端获取用户菜单信息api
const getUserMenuApi = () => {
// 假设只有about菜单
return Promise.resolve([
{
path: "/about",
name: "About",
component: "../views/About.vue",
title: "关于",
},
]);
};
const userMenus = await getUserMenuApi();
commit("setUserMenus", userMenus);
return userMenus;
},
},
});
使用
创建完路由后需要在 main.js
导入使用
js
复制代码
import router from "./router";
// vue2写法
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
// vue3写法
const app = createApp(App);
app.use(router).mount("#app");
在没登录的情况下,去about
和userlist
页面会重定向到登录页。
给本地添加token
,模拟登录完成后,因为用户有about
的菜单,所以去 about
页面是没问题的,去 userlist
页面则会重定向到没有权限页面。
总结
优点:
系统角色可以不固定,支持动态添加角色。适用于后台大型管理系统。
缺点:
实现相对复杂,而且需要后端配合的更多。
总结
第一种通过 beforeEach
实时监测用户权限的方式优势是实现简单,路由的权限在 meta
的 roles
里面配置。用户信息里面只需要包含当前用户的角色。缺点呢也很明显,系统所拥有的角色必须事先知道,然后以死代码的形式配置在 routes
里面,不支持动态添加角色。笔者觉得如果是做角色固定的系统是非常好的,比如什么图书管理系统、教师管理系统等。
第二种通过 通过 addRoutes
动态添加路由实现角色权限的方式优势是系统角色可以不固定,支持动态添加角色。很适用于后台大型管理系统,可以实时创建角色分配菜单。
比如笔者做的低代码后台管理系统,就有实时创建菜单、角色、用户等一系列功能。
image.png
当然、缺点就是实现起来比较复杂,需要后端一起参与设计。
小伙伴们,如果你现在的系统需要做用户角色权限,你会选择哪种方案呢?