前端框架对比系列之vue和react的页面角色权限控制(一)

前言

对于一个后台管理系统,尤其是复杂的后台管理系统,一定会涉及到复杂的页面角色权限管理。例如登录的用户能看到系统的哪些页面,不能看到系统的哪些页面。都是通过前端获取到当前登陆角色的用户信息,然后在通过路由守卫拦截,完成对要访问的页面的控制。页面角色权限控制一般通过两种方法实现 (当前文章只讨论第一种方式,第二种方式会在后期文章中继续更新):

  1. 通过前端本地的路由route 配置roles 等信息,然后获取当前用户信息,利用全局路由守卫对获取的用户信息进行匹配,检测当前用户的权限;
  2. 通过动态添加路由的方法,即调用后端接口返回当前用户角色拥有的所有页面菜单信息,将菜单格式化成路由之后,再动态的添加到前端系统中,这种方式会更加灵活。

1. vue2vue3实现页面的角色权限控制

对于 vue 框架而言,就是在 route 里面配置 roles,然后利用 beforeEach 全局路由守卫来实时检测用户权限。大致的思路如下: 首先在 routes 里面事先定义好路由的权限,然后在 beforeEach 里面进行权限逻辑判断。看用户所拥有的角色和我们配置在路由里面的 roles 是否相匹配。匹配则允许进入,不匹配则重定向到无权限页面

1.1 定义 routes

可以在 meta 里面可以定义我们需要的元数据roles。也就是说进入该路由用户所需要具备的角色权限,没定义则代表任意角色都能进入。

ts 复制代码
// 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;

同时在首页添加导航按钮,便于验证页面的跳转:

ts 复制代码
// 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 全局前置守卫。主要的思路如下:

  1. 首先判断前往的页面是否需要登录,需要登录就进一步判断当前系统是否有token,没有token则重定向到登录页
  2. 如果有token,则进一步判断是否有用户信息,如果没有用户信息就通过接口获取用户信息 (此处是通过结合vuex,实现用户信息缓存)
  3. 有了用户信息后,再判断进入页面需要的角色是否和用户信息里面的角色相匹配,匹配则进入页面,不匹配则进入系统的无权限提示页面。
  4. 注意 vue2 和 vue3版本下的实例化 Router 方式的差别

vue2 版本如下:

ts 复制代码
// 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 版本如下:

ts 复制代码
// 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 版本如下:

js 复制代码
// 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 版本如下:

js 复制代码
// 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 版本如下:

js 复制代码
// main.js
...
import store from './store'
...
new Vue({
  store,
  router,
  render: h => h(App),
}).$mount('#app')

vue3 版本如下:

js 复制代码
// 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 就是进入该路由用户所需要具备的角色权限,没定义则代表任意角色都能进入。

js 复制代码
// 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 框架其实是一样的,区别在于:

  1. react 不具备vue路由守卫的前置处理能力,需要自定义高阶组件实现;
  2. 对于用户角色信息的的缓存处理,需要依赖react-redux;

如下所示,实现一个 Router 组件,可以通过 useDispatch 钩子获取触发actions的方法调角色信息接口,然后在 Auth 组件中通过 useSelector 获取角色信息(替代connect 取缓存数据的方法)。

js 复制代码
// 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>
  );
}
js 复制代码
// 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组件指定路由渲染区域:

js 复制代码
/* 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:

js 复制代码
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:

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.jsstore/reducers/index.js:

js 复制代码
// store/reducers/index.js
import { combineReducers } from "redux";
import user from "./user";

export default combineReducers({
  user
});
js 复制代码
// 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;
  }
}
相关推荐
无双_Joney17 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥18 分钟前
前端必学的 CSS Grid 布局体系
前端·css
EMT18 分钟前
在 Vue 项目中使用 URL Query 保存和恢复搜索条件
javascript·vue.js
ccnocare20 分钟前
选择文件夹路径
前端
艾小码20 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月21 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁24 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅24 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸26 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端