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

前言

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

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

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

对于 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 全局前置守卫。主要的思路如下:

  1. 首先判断前往的页面是否需要登录,需要登录就进一步判断当前系统是否有token,没有token则重定向到登录页
  2. 如果有token,则进一步判断是否有用户信息,如果没有用户信息就通过接口获取用户信息 (此处是通过结合vuex,实现用户信息缓存)
  3. 有了用户信息后,再判断进入页面需要的角色是否和用户信息里面的角色相匹配,匹配则进入页面,不匹配则进入系统的无权限提示页面。
  4. 注意 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 框架其实是一样的,区别在于:

  1. react 不具备vue路由守卫的前置处理能力,需要自定义高阶组件实现;
  2. 对于用户角色信息的的缓存处理,需要依赖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.jsstore/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 作者:阿镇吃橙子

(0)
上一篇 2024年3月20日 下午4:43
下一篇 2024年3月20日 下午4:54

相关推荐

发表评论

登录后才能评论