资讯专栏INFORMATION COLUMN

React中的权限组件设计问题小结

3403771864 / 391人阅读

  背景

       在项目中要求在后台系统控制管理权限。在之前做过的后台管理系统权限控制是用Vue,这样的话就可以用路由钩子里做权限比对和拦截处理。但这次我们说的是在一个后台系统需要加入权限管理控制,技术栈是React。现在我们就看看实现过程吧。

  原代码基于 react 16.x、dva 2.4.1 实现,所以本文是参考了ant-design-pro v1内部对权限管理的实现

  所谓的权限控制是什么?

  一般后台管理系统的权限涉及到两种:

  资源权限

  数据权限

  资源权限一般指菜单、页面、按钮等的可见权限。

  数据权限一般指对于不同用户,同一页面上看到的数据不同。

  本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:

  侧边栏菜单

  路由权限

  用户对于前端权限控制就是左侧菜单的可见与否,并不是如此。简单来说,当用户guest没有路由/setting的访问权限,这样就可以知道/setting的完整路径,就可以直接访问进入。这样没有任何作用啊。这部分其实就属于路由层面的权限控制。

  实现思路

  关于前端权限控制一般有两种方案:

  前端固定路由表和权限配置,由后端提供用户权限标识

  后端提供权限和路由信息结构接口,动态生成权限和菜单

  我们这里采用的是第一种方案,服务只下发当前用户拥有的角色就可以了,路由表和权限的处理统一在前端处理。

  整体实现思路也比较简单:现有权限(currentAuthority)和准入权限(authority)做比较,如果匹配则渲染和准入权限匹配的组件,否则渲染无权限组件(403 页面)

  路由权限

  既然是路由相关的权限控制,我们免不了先看一下当前的路由表:

  {
  "name": "活动列表",
  "path": "/activity-mgmt/list",
  "key": "/activity-mgmt/list",
  "exact": true,
  "authority": [
  "admin"
  ],
  "component": ƒ LoadableComponent(props),
  "inherited": false,
  "hideInBreadcrumb": false
  },
  {
  "name": "优惠券管理",
  "path": "/coupon-mgmt/coupon-rule-bplist",
  "key": "/coupon-mgmt/coupon-rule-bplist",
  "exact": true,
  "authority": [
  "admin",
  "coupon"
  ],
  "component": ƒ LoadableComponent(props),
  "inherited": true,
  "hideInBreadcrumb": false
  },
  {
  "name": "营销录入系统",
  "path": "/marketRule-manage",
  "key": "/marketRule-manage",
  "exact": true,
  "component": ƒ LoadableComponent(props),
  "inherited": true,
  "hideInBreadcrumb": false
  }

  这份路由表其实是我从控制台 copy 过来的,内部做了很多的转换处理,但最终生成的就是上面这个对象。

  这里每一级菜单都加了一个authority字段来标识允许访问的角色。component代表路由对应的组件:

  import React, { createElement } from "react"
  import Loadable from "react-loadable"
  "/activity-mgmt/list": {
  component: dynamicWrapper(app, ["activityMgmt"], () => import("../routes/activity-mgmt/list"))
  },
  // 动态引用组件并注册model
  const dynamicWrapper = (app, models, component) => {
  // register models
  models.forEach(model => {
  if (modelNotExisted(app, model)) {
  // eslint-disable-next-line
  app.model(require(`../models/${model}`).default)
  }
  })
  // () => require('module')
  // transformed by babel-plugin-dynamic-import-node-sync
  // 需要将routerData塞到props中
  if (component.toString().indexOf(".then(") < 0) {
  return props => {
  return createElement(component().default, {
  ...props,
  routerData: getRouterDataCache(app)
  })
  }
  }
  // () => import('module')
  return Loadable({
  loader: () => {
  return component().then(raw => {
  const Component = raw.default || raw
  return props =>
  createElement(Component, {
  ...props,
  routerData: getRouterDataCache(app)
  })
  })
  },
  // 全局loading
  loading: () => {
  return (
  <div
  style={{
  display: "flex",
  justifyContent: "center",
  alignItems: "center"
  }}
  >
  <Spin size="large" className="global-spin" />
  </div>
  )
  }
  })
  }

  复制代码

  有了路由表这份基础数据,下面就让我们来看下如何通过一步步的改造给原有系统注入权限。

  先从src/router.js这个入口开始着手:

   // 原src/router.js
  import dynamic from "dva/dynamic"
  import { Redirect, Route, routerRedux, Switch } from "dva/router"
  import PropTypes from "prop-types"
  import React from "react"
  import NoMatch from "./components/no-match"
  import App from "./routes/app"
  const { ConnectedRouter } = routerRedux
  const RouterConfig = ({ history, app }) => {
  const routes = [
  {
  path: "activity-management",
  models: () => [import("@/models/activityManagement")],
  component: () => import("./routes/activity-mgmt")
  },
  {
  path: "coupon-management",
  models: () => [import("@/models/couponManagement")],
  component: () => import("./routes/coupon-mgmt")
  },
  {
  path: "order-management",
  models: () => [import("@/models/orderManagement")],
  component: () => import("./routes/order-maint")
  },
  {
  path: "merchant-management",
  models: () => [import("@/models/merchantManagement")],
  component: () => import("./routes/merchant-mgmt")
  }
  // ...
  ]
  return (
  <ConnectedRouter history={history}>
  <App>
  <Switch>
  {routes.map(({ path, ...dynamics }, key) => (
  <Route
  key={key}
  path={`/${path}`}
  component={dynamic({</p>
  <p>
  app,</p>
  <p>
  ...dynamics</p>
  <p>
  })}
  />
  ))}
  <Route component={NoMatch} />
  </Switch>
  </App>
  </ConnectedRouter>
  )
  }
  RouterConfig.propTypes = {
  history: PropTypes.object,
  app: PropTypes.object
  }
  export default RouterConfig

  这是一个非常常规的路由配置,既然要加入权限,比较合适的方式就是包一个高阶组件AuthorizedRoute。然后router.js就可以更替为:

  function RouterConfig({ history, app }) {
  const routerData = getRouterData(app)
  const BasicLayout = routerData["/"].component
  return (
  <ConnectedRouter history={history}>
  <Switch>
  <AuthorizedRoute path="/" render={props => <BasicLayout {...props} />} />
  </Switch>
  </ConnectedRouter>
  )
  }

  来看下AuthorizedRoute的大致实现:

  const AuthorizedRoute = ({
  component: Component,
  authority,
  redirectPath,
  {...rest}
  }) => {
  if (authority === currentAuthority) {
  return (
  <Route
  {...rest}
  render={props => <Component {...props} />} />
  )
  } else {
  return (
  <Route {...rest} render={() =>
  <Redirect to={redirectPath} />
  } />
  )
  }
  }

  我们看一下这个组件有什么问题:页面可能允许多个角色访问,用户拥有的角色也可能是多个(可能是字符串,也可呢是数组)。

  直接在组件中判断显然不太合适,我们把这部分逻辑抽离出来:

  /**
  * 通用权限检查方法
  * Common check permissions method
  * @param { 菜单访问需要的权限 } authority
  * @param { 当前角色拥有的权限 } currentAuthority
  * @param { 通过的组件 Passing components } target
  * @param { 未通过的组件 no pass components } Exception
  */
  const checkPermissions = (authority, currentAuthority, target, Exception) => {
  console.log("checkPermissions -----> authority", authority)
  console.log("currentAuthority", currentAuthority)
  console.log("target", target)
  console.log("Exception", Exception)
  // 没有判定权限.默认查看所有
  // Retirement authority, return target;
  if (!authority) {
  return target
  }
  // 数组处理
  if (Array.isArray(authority)) {
  // 该菜单可由多个角色访问
  if (authority.indexOf(currentAuthority) >= 0) {
  return target
  }
  // 当前用户同时拥有多个角色
  if (Array.isArray(currentAuthority)) {
  for (let i = 0; i < currentAuthority.length; i += 1) {
  const element = currentAuthority[i]
  // 菜单访问需要的角色权限 < ------ > 当前用户拥有的角色
  if (authority.indexOf(element) >= 0) {
  return target
  }
  }
  }
  return Exception
  }
  // string 处理
  if (typeof authority === "string") {
  if (authority === currentAuthority) {
  return target
  }
  if (Array.isArray(currentAuthority)) {
  for (let i = 0; i < currentAuthority.length; i += 1) {
  const element = currentAuthority[i]
  if (authority.indexOf(element) >= 0) {
  return target
  }
  }
  }
  return Exception
  }
  throw new Error("unsupported parameters")
  }
  const check = (authority, target, Exception) => {
  return checkPermissions(authority, CURRENT, target, Exception)
  }

  首先如果路由表中没有authority字段默认都可以访问。

  接着分别对authority为字符串和数组的情况做了处理,其实就是简单的查找匹配,匹配到了就可以访问,匹配不到就返回Exception,也就是我们自定义的异常页面。

  有一个点一直没有提:用户当前角色权限currentAuthority如何获取?这个是在页面初始化时从接口读取,然后存到store中

  有了这块逻辑,我们对刚刚的AuthorizedRoute做一下改造。首先抽象一个Authorized组件,对权限校验逻辑做一下封装:

  import React from "react"
  import CheckPermissions from "./CheckPermissions"
  class Authorized extends React.Component {
  render() {
  const { children, authority, noMatch = null } = this.props
  const childrenRender = typeof children === "undefined" ? null : children
  return CheckPermissions(authority, childrenRender, noMatch)
  }
  }
  export default Authorized

  接着AuthorizedRoute可直接使用Authorized组件:

  import React from "react"
  import { Redirect, Route } from "react-router-dom"
  import Authorized from "./Authorized"
  class AuthorizedRoute extends React.Component {
  render() {
  const { component: Component, render, authority, redirectPath, ...rest } = this.props
  return (
  <Authorized
  authority={authority}
  noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
  >
  <Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} />
  </Authorized>
  )
  }
  }
  export default AuthorizedRoute

  这里采用了render props的方式:如果提供了component props就用component渲染,否则使用render渲染。

  菜单权限

  菜单权限的处理相对就简单很多了,统一集成到SiderMenu组件处理:

  export default class SiderMenu extends PureComponent {
  constructor(props) {
  super(props)
  }
  /**
  * get SubMenu or Item
  */
  getSubMenuOrItem = item => {
  if (item.children && item.children.some(child => child.name)) {
  const childrenItems = this.getNavMenuItems(item.children)
  // 当无子菜单时就不展示菜单
  if (childrenItems && childrenItems.length > 0) {
  return (
  <SubMenu
  title={
  item.icon ? (
  <span>
  {getIcon(item.icon)}
  <span>{item.name}</span>
  </span>
  ) : (
  item.name
  )
  }
  key={item.path}
  >
  {childrenItems}
  </SubMenu>
  )
  }
  return null
  }
  return <Menu.Item key={item.path}>{this.getMenuItemPath(item)}</Menu.Item>
  }
  /**
  * 获得菜单子节点
  * @memberof SiderMenu
  */
  getNavMenuItems = menusData => {
  if (!menusData) {
  return []
  }
  return menusData
  .filter(item => item.name && !item.hideInMenu)
  .map(item => {
  // make dom
  const ItemDom = this.getSubMenuOrItem(item)
  return this.checkPermissionItem(item.authority, ItemDom)
  })
  .filter(item => item)
  }
  /**
  *
  * @description 菜单权限过滤
  * @param {*} authority
  * @param {*} ItemDom
  * @memberof SiderMenu
  */
  checkPermissionItem = (authority, ItemDom) => {
  const { Authorized } = this.props
  if (Authorized && Authorized.check) {
  const { check } = Authorized
  return check(authority, ItemDom)
  }
  return ItemDom
  }
  render() {
  // ...
  return
  <Sider
  trigger={null}
  collapsible
  collapsed={collapsed}
  breakpoint="lg"
  onCollapse={onCollapse}
  className={siderClass}
  >
  <div className="logo">
  <Link to="/home" className="logo-link">
  {!collapsed && <h1>冯言冯语</h1>}
  </Link>
  </div>
  <Menu
  key="Menu"
  theme={theme}
  mode={mode}
  {...menuProps}
  onOpenChange={this.handleOpenChange}
  selectedKeys={selectedKeys}
  >
  {this.getNavMenuItems(menuData)}
  </Menu>
  </Sider>
  }
  }

      注意在核心代码中checkPermissionItem就是实现菜单权限的关键,就在同样用到了上文中的check方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/127834.html

相关文章

  • 【单页面博客从前端到后端】基于 DVA+ANTD 搭建博客前后台界面

    摘要:在的的配置中添加自定义主题由脚手架和官网介绍,我们已经自己配置并新建好了主题文件。单页面博客从前端到后端环境搭建单页面博客从前端到后端基于搭建博客前后台界面单页面博客从前端到后端基于和的权限验证与的设计 在上篇文章我们已经搭建好了基础的开发环境,接下来会介绍如何引入 DVA 和 ANTD ,以及在引入过程中需要注意的问题。这里只会详细的书写部分组件,其他的组件都是大同小异。你可以在 g...

    zqhxuyuan 评论0 收藏0
  • 面试小结(一)

    摘要:面试问到的问题继承的几种方法,,原形继承面向对象的几种方法五种方式对象字面量创建实例对象构造函数工厂模式用一个函数,通过传递参数返回对象。打包原理打包原理把所有依赖打包成一个文件,通过代码分割成单元片段并按需加载。 面试问到的问题:1、继承的几种方法; Call,apply,原形继承; 2、面向对象的几种方法; 五种方式: 1)对象字面量:var obj={}; 2)创建实例对象:va...

    xiaodao 评论0 收藏0
  • 面试小结(一)

    摘要:面试问到的问题继承的几种方法,,原形继承面向对象的几种方法五种方式对象字面量创建实例对象构造函数工厂模式用一个函数,通过传递参数返回对象。打包原理打包原理把所有依赖打包成一个文件,通过代码分割成单元片段并按需加载。 面试问到的问题:1、继承的几种方法; Call,apply,原形继承; 2、面向对象的几种方法; 五种方式: 1)对象字面量:var obj={}; 2)创建实例对象:va...

    SnaiLiu 评论0 收藏0
  • 面试小结(一)

    摘要:面试问到的问题继承的几种方法,,原形继承面向对象的几种方法五种方式对象字面量创建实例对象构造函数工厂模式用一个函数,通过传递参数返回对象。打包原理打包原理把所有依赖打包成一个文件,通过代码分割成单元片段并按需加载。 面试问到的问题:1、继承的几种方法; Call,apply,原形继承; 2、面向对象的几种方法; 五种方式: 1)对象字面量:var obj={}; 2)创建实例对象:va...

    shmily 评论0 收藏0
  • react搭建后台管理(react初窥)

    摘要:前言以前一直是用进行的开发于是决定年后弄一弄所以年后这段时间也就一直瞎弄可算是看到成果了本来是想写一个类似仿今日头条那样的项目来入手后来又寻思还不如写个后台管理呢。于是乎自己便着手简单的搭建了一个集中设置的版本。 前言 以前一直是用vue进行的开发, 于是决定年后弄一弄react, 所以年后这段时间也就一直瞎弄react, 可算是看到成果了 本来是想写一个 类似 Vue仿今日头条 那样...

    wangjuntytl 评论0 收藏0

发表评论

0条评论

3403771864

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<