前言
对于一个后台管理系统,尤其是复杂的后台管理系统,一定会涉及到复杂的页面角色权限管理。例如登录的用户能看到系统的哪些页面,不能看到系统的哪些页面。都是通过前端获取到当前登陆角色的用户信息,然后在通过路由守卫拦截,完成对要访问的页面的控制。页面角色权限控制一般通过两种方法实现 (当前文章只讨论第一种方式,第二种方式会在后期文章中继续更新
):
- 通过前端本地的路由route 配置roles 等信息,然后获取当前用户信息,利用全局路由守卫对获取的用户信息进行匹配,检测当前用户的权限;
- 通过动态添加路由的方法,即调用后端接口返回当前用户角色拥有的所有页面菜单信息,将菜单格式化成路由之后,再动态的添加到前端系统中,这种方式会更加灵活。
1. vue2
和vue3
实现页面的角色权限控制
对于 vue
框架而言,就是在 route
里面配置 roles
,然后利用 beforeEach
全局路由守卫来实时检测用户权限。大致的思路如下: 首先在 routes
里面事先定义好路由的权限,然后在 beforeEach
里面进行权限逻辑判断。看用户所拥有的角色和我们配置在路由里面的 roles
是否相匹配。匹配则允许进入,不匹配则重定向到无权限页面。
1.1 定义 routes
可以在 meta
里面可以定义我们需要的元数据roles
。也就是说进入该路由用户所需要具备的角色权限,没定义则代表任意角色都能进入。
// router/routes.js
import Home from "../components/Home.vue"
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: "/home",
name: "home",
component: Home,
meta: {
needLogin: false, // 不需要登录
title: "首页",
}
},
{
path: "/login",
name: "login",
component: () => import(/* webpackChunkName: "login" */ "../components/Login.vue"), // 路由懒加载
meta: {
needLogin: false, // 不需要登录
title: "登录"
}
},
{
path: "/about",
name: "about",
component: () => import(/* webpackChunkName: "about" */ "../components/About.vue"), // 路由懒加载
meta: {
needLogin: true, // 需要登录
title: "关于",
roles: ['admin', 'master', 'developer']
}
},
{
path: "/nopermission", // 没权限就进入该页面
name: "NoPermission",
component: () => import(/* webpackChunkName: "about" */ "../components/NoPermission.vue"), // 路由懒加载
meta: {
needLogin: false, // 需要登录
title: "暂无权限",
},
}
];
export default routes;
同时在首页添加导航按钮,便于验证页面的跳转:
// app.vue
<template>
<div id="app">
<div class="app-tab">
<h3>vue2 首页导航</h3>
<div style="display: flex; justifyContent: space-between">
<router-link to="/login">跳转login</router-link>
<router-link to="/about">跳转about</router-link>
<router-link to="/nopermission">跳转nopermission</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
1.2 实例化Router
&& 定义路由拦截鉴权
创建好路由之后,就可以来定义路由拦截。主要通过 beforeEach
全局前置守卫。因为只要页面发生跳转都会进入 beforeEach
全局前置守卫。主要的思路如下:
- 首先判断前往的页面是否需要登录,需要登录就进一步判断当前系统是否有token,没有token则重定向到登录页
- 如果有token,则进一步判断是否有用户信息,如果没有用户信息就通过接口获取用户信息 (
此处是通过结合vuex,实现用户信息缓存
) - 有了用户信息后,再判断
进入页面需要的角色是否和用户信息里面的角色相匹配
,匹配则进入页面,不匹配则进入系统的无权限提示页面。 - 注意 vue2 和 vue3版本下的实例化 Router 方式的差别
vue2 版本如下:
// router/index.js
import VueRouter from "vue-router";
import routes from "./routes"
import Vue from 'vue';
import store from "../store";
Vue.use(VueRouter);
const router = new VueRouter({
mode: "hash",
routes,
});
router.beforeEach( async (to, from, next) => {
// 判断是否需要登陆
if (to.meta.needLogin) {
const token = localStorage.getItem("vue2-demo-token"); // 本地控制添加token
if (token) {
// 获取用户信息,首先从store里面获取; 如果没有就通过模拟接口获取
let userInfo = store.getters["getUserInfo"];
if (!userInfo) {
userInfo = await store.dispatch("getUserInfoAction");
}
console.log('userInfo =======>', userInfo);
// 通过拿到的用户信息判断页面跳转权限
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;
}
});
export default router;
vue3 版本如下:
// router/index.js
import { createRouter, createWebHistory , createWebHashHistory} from "vue-router";
import routes from "./routes"
import store from '../store'
const router = createRouter({
history: createWebHashHistory(),
routes,
});
router.beforeEach( async (to, from, next) => {
// 判断是否需要登陆
if (to.meta.needLogin) {
const token = localStorage.getItem("vue3-demo-token");
if (token) {
// 获取用户信息,从store里面获取; 如果没有用户信息就通过接口获取
let userInfo = store.getters["getUserInfo"];
if (!userInfo) {
userInfo = await store.dispatch("getUserInfoAction");
}
console.log('userInfo =======>', userInfo);
// 通过拿到的用户信息判断页面跳转权限
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;
}
});
export default router;
1.3 用户信息的存储依赖 vuex
实现
首先新建一个store
文件,实现用户信息的缓存。如下图中 getUserInfoAction
方法实现了一个promise 模拟后台接口请求,用来获得用户信息的数据,获取成功之后,通过 commit
来缓存用户信息数据 userInfo
。
vue2 版本如下:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
userInfo: null
},
getters: {
getUserInfo: (state) => state.userInfo,
},
mutations: {
setUserInfo(state, payload) {
state.userInfo = payload;
},
},
actions: {
// 获取用户信息的action (模拟实现后台接口)
async getUserInfoAction({ commit }) {
const getUserInfoApi = () => {
return Promise.resolve({ role: "master", name: "jack" }); // 假设角色为 manage
};
const userInfo = await getUserInfoApi();
commit("setUserInfo", userInfo);
return userInfo;
},
},
modules: {
}
})
vue3 版本如下:
// store/index.js
import { createStore } from "vuex";
export default createStore({
state: {
userInfo: null,
},
getters: {
getUserInfo: (state) => state.userInfo,
},
mutations: {
setUserInfo(state, payload) {
state.userInfo = payload;
},
},
actions: {
// 获取用户信息的action, 模拟后端接口获取用户信息
async getUserInfoAction({ commit }) {
const getUserInfoApi = () => {
return Promise.resolve({ role: "master", name: "jack" }); // 假设角色为 master
};
const userInfo = await getUserInfoApi();
commit("setUserInfo", userInfo);
return userInfo;
},
},
});
然后在在项目的入口文件 main.js
中引入store
vue2 版本如下:
// main.js
...
import store from './store'
...
new Vue({
store,
router,
render: h => h(App),
}).$mount('#app')
vue3 版本如下:
// main.js
...
import store from './store'
...
const app = createApp(App);
app.use(store);
app.use(router);
app.mount('#app')
2. react
实现页面的角色权限控制
当前的react-router-dom
版本为5.x.x, 我们知道react 的路由不存在路由守卫的前置处理,所以需要实现一个高阶组件,大致的思路如下:首先在routes
里面定义好路由的权限,然后在高阶组件里面进行权限逻辑判断。看用户所拥有的角色和我们配置的 roles
是否相匹配。匹配则允许进入,不匹配则重定向到无权限页面
2.1 定义 routes
定义routes 的思路和上述vue框架的思路比较类似,就是在 meta
中设置元数据 roles。定义的roles 就是进入该路由用户所需要具备的角色权限,没定义则代表任意角色都能进入。
// router/routes.js
import Home from "../pages/Home";
import About from "../pages/About";
import Login from "../pages/Login";
import NoPermission from "../pages/NoPermission";
const routes = [
{
path: "/home",
component: Home,
meta: {
title: "首页",
needLogin: false,
},
},
{
path: "/about",
component: About,
meta: {
title: "关于",
needLogin: true,
roles: ['admin', 'master', 'developer']
},
},
{
path: "/login",
component: Login,
meta: {
title: "登录",
needLogin: false,
},
},
{
path: "/nopermission",
component: NoPermission,
meta: {
title: "没有访问权限页面",
needLogin: false,
},
},
{
path: '/',
redirect: '/home'
},
];
export default routes;
2.2 定义高阶组件 Auth
实现一个组件Auth
,用来处理角色权限鉴权逻辑,结合 react-redux
做用户信息缓存,通过获取缓存的用户数据信息来对角色权限进行路由分配处理。整体的思路和vue 框架其实是一样的,区别在于:
- react 不具备vue路由守卫的前置处理能力,需要自定义高阶组件实现;
- 对于用户角色信息的的缓存处理,需要依赖react-redux;
如下所示,实现一个 Router
组件,可以通过 useDispatch
钩子获取触发actions
的方法调角色信息接口,然后在 Auth
组件中通过 useSelector
获取角色信息(替代connect 取缓存数据的方法)。
// router/index.js
import routes from "./routes";
import { Switch } from "react-router-dom";
import { useEffect } from 'react'
import Auth from "./auth";
import { useDispatch } from "react-redux";
import { getUserInfo } from "../store/actions/user";
export default function Router() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getUserInfo()); // 调用后会设置用户信息为 { role: "admin", name: "jack" }
}, []);
return (
<div >
<Switch>
{routes.map((route) => {
return (
// 路由鉴权
<Auth key={route.path} {...route}></Auth>
);
})}
</Switch>
</div>
);
}
// router/auth.js
import { Route, Redirect } from "react-router-dom";
import { useSelector } from 'react-redux'
export default function Auth(props) {
const {
component: Component,
path,
meta,
routes,
redirect,
exact,
strict,
} = props;
// 获取用户信息
const userInfo = useSelector((state) => state.user);
console.log('userInfo =====>', userInfo)
// 设置网页标题
if (meta && meta.title) {
document.title = meta.title;
}
// 重定向
if (redirect) {
return <Redirect to={redirect} />;
}
// 判断是否需要登录
if (meta && meta.needLogin) {
const token = localStorage.getItem("react-demo-token");
// 没登录去登录页
if (!token) {
return <Redirect to="/login" />;
}
}
// 路由需要角色、并且当前有用户信息 并且角色不匹配则去没有权限页面
if (meta && meta.roles && userInfo && !meta.roles.includes(userInfo.role)) {
return <Redirect to="/nopermission" />;
}
return (
<Route
path={path}
exact={exact}
strict={strict}
render={(props) => <Component {...props} routes={routes} />}
></Route>
);
}
2.3 App.jsx引入store 和 Router 组件
要拿到全局缓存数据,必须通过 Provider
引入 store
,同时引入自定义的Router组件
指定路由渲染区域:
/* eslint-disable no-unused-vars */
import { ConfigProvider } from 'antd';
import { Provider } from "react-redux";
import store from './store/index'
import Router from './router';
import { Link } from 'react-router-dom'
function App() {
return (
<ConfigProvider theme={{ token: { colorPrimary: '#ff721f' } }}>
<Provider store={store}>
<div className="App">
<div className='app-tab' style={{ borderBottom: 'solid 1px grey' }}>
<h1>react 首页导航</h1>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<Link to="/login">跳转到登陆页</Link>
<Link to="/about">跳转到about页</Link>
<Link to="/noPermission">跳转到noPermission页</Link>
</div>
</div>
{/* 路由渲染区域 */}
<Router></Router>
</div>
</Provider>
</ConfigProvider>
);
}
export default App;
2.4 store 的配置
新建 store
文件目录,在该目录下新增index.js
,导出store:
import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'
import reducer from './reducers'
const store = createStore(reducer, applyMiddleware(thunk));
export default store
在store 目录下新建 actions/user.js
:
// store/actions/user.js
export const getUserInfo = () => async (dispatch) => {
// 这里模拟调用后端接口获取了用户信息数据:
const userInfo = await Promise.resolve({ role: "admin", name: "jack" });
dispatch(setUserInfo(userInfo));
};
export const setUserInfo = (userInfo) => {
return {
type: 'SET_USERINFO',
userInfo
};
};
在store 目录下新建 reducers/user.js
和 store/reducers/index.js
:
// store/reducers/index.js
import { combineReducers } from "redux";
import user from "./user";
export default combineReducers({
user
});
// store/reducers/user.js
const initUserInfo = {
name: "",
role: ""
};
export default function user(state = initUserInfo, action) {
switch (action.type) {
case 'SET_USERINFO':
return {
...state,
name: action.userInfo.name,
role: action.userInfo.role
};
default:
return state;
}
}
原文链接:https://juejin.cn/post/7347616740852924466 作者:阿镇吃橙子