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

目录
  • 背景
  • 所谓的权限控制是什么?
  • 实现思路
  • 路由权限
  • 菜单权限

背景

权限管理是中后台系统中常见的需求之一。之前做过基于 Vue 的后台管理系统权限控制,基本思路就是在一些路由钩子里做权限比对和拦截处理。

最近维护的一个后台系统需要加入权限管理控制,这次技术栈是React,我刚开始是在网上搜索一些React路由权限控制,但是没找到比较好的方案或思路。

这时想到ant design pro内部实现过权限管理,因此就专门花时间翻阅了一波源码,并在此基础上逐渐完成了这次的权限管理。

整个过程也是遇到了很多问题,本文主要来做一下此次改造工作的总结。

原代码基于 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({
                app,
                ...dynamics
              })}
            />
          ))}
          <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方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。

到此这篇关于React中的权限组件设计的文章就介绍到这了,更多相关React权限组件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • react高阶组件经典应用之权限控制详解

    前言 所谓高级组件,即:接受一个组件作为参数,并且其返回值也为一个react组件 而大家应该都知道,权限控制算是软件项目中的常用功能了.在网站中,权限控制一般分为两个维度:页面级别和页面元素级别. 我们来说说页面元素粒度的权限控制.在某个页面中,有个"创建用户"的按钮,管理员才能看到. 一般想到的做法类似这样 class Page extends Component{ render() { let hasCreatePermission = tool.getAuth("cre

  • React 组件权限控制的实现

    目录 前话 正文 1. 控制方式 1.1 直接计算 1.2 通用权限Hoc 1.3 权限包裹组件 2. 控制结果 2.1 显隐控制 2.2 自定义渲染 3. 权限数据 3.1 静态权限 3.2 动态权限 前话 具备用户体系的业务系统往往需要具备权限控制的能力.前后不分离的开发模式权限控制主要由后端结合模版引擎实现,而前后分离的开发模式由前后两端协商权限配置分别进行数据权限控制和组件权限控制. 正文 权限配置格式不限,为进行后续演示本篇设定权限配置如下: export type IAuthConf

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

    目录 背景 所谓的权限控制是什么? 实现思路 路由权限 菜单权限 背景 权限管理是中后台系统中常见的需求之一.之前做过基于 Vue 的后台管理系统权限控制,基本思路就是在一些路由钩子里做权限比对和拦截处理. 最近维护的一个后台系统需要加入权限管理控制,这次技术栈是React,我刚开始是在网上搜索一些React路由权限控制,但是没找到比较好的方案或思路. 这时想到ant design pro内部实现过权限管理,因此就专门花时间翻阅了一波源码,并在此基础上逐渐完成了这次的权限管理. 整个过程也是遇到

  • React中使用antd组件的方法

    目录 antd 使用antd antd antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品. 国内镜像为:https://ant-design.gitee.io/docs/react/introduce-cn速度很快 进入网页点击组件就可以看到网页需要的各种配件,如按钮.导航栏等等,并且配有各种使用方法的API,目前已经更新到4.22版本,原本是只支持react不过后来也支持vue了.3.几版本的文档说明会更加详细antd还可以更改主题颜色

  • 深入理解React中es6创建组件this的方法

    首发于:https://mingjiezhang.github.io/. 在JavaScript中,this对象是运行时基于函数的执行环境(也就是上下文)绑定的. 从react中的demo说起 Facebook最近一次更新react时,将es6中的class加入了组件的创建方式当中.Facebook也推荐组件创建使用通过定义一个继承自 React.Component 的class来定义一个组件类.官方的demo: class LikeButton extends React.Component

  • 浅析React中的受控组件和非受控组件

    非受控组件 表单数据由DOM本身处理.即不受setState()的控制,与传统的HTML表单输入相似,input输入值即显示最新值(使用 ref从DOM获取表单值) 1.非受控组件 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body&g

  • Vue3中10多种组件通讯方法小结

    目录 Props emits expose / ref Non-Props 单个根元素的情况 多个元素的情况 v-model 单值的情况 多个 v-model 绑定 v-model 修饰符 插槽 slot 默认插槽 具名插槽 作用域插槽 provide / inject 总线 bus getCurrentInstance Vuex State Getter Mutation Action Module Pinia 安装 注册 mitt.js 安装 使用 本文讲解 Vue 3.2 组件多种通讯方式

  • 一文带你了解React中的函数组件

    目录 1. 创建方式 2. 函数组件代替类组件 3. 自定义 Hook 之 useUpdate 补充:函数组件代替 class 组件 总结 1. 创建方式 // 写法一 const Hello = (props) => { return <div>{props.message}</div> } // 写法二 const Hello = props => <div>{props.message}</div> // 写法三 function Hell

  • 在 React 中使用 Redux 解决的问题小结

    目录 在 React 中使用 Redux 解决的问题 在 React 项目中加入 Redux 的好处 React + Redux 安装 Redux React 中 Redux 的工作流程 React 计数器案例 使用 Redux Provide 组件 connect 方法 使用 bindActionCreators 方法继续简化 代码重构 为 Action 传递参数 Redux 弹出框 初始化静态内容 添加默认隐藏状态 定义操作按钮 衍生的问题 拆分合并 reducer 拆分 合并 调整组件 在

  • 浅谈React中组件逻辑复用的那些事儿

    基本每个开发者都需要考虑逻辑复用的问题,否则你的项目中将充斥着大量的重复代码.那么 React 是怎么复用组件逻辑的呢?本文将一一介绍 React 复用组件逻辑的几种方法,希望你读完之后能够有所收获.如果你对这些内容已经非常清楚,那么略过本文即可. 我已尽量对文中的代码和内容进行了校验,但是因为自身知识水平限制,难免有错误,欢迎在评论区指正. 1. Mixins Mixins 事实上是 React.createClass 的产物了.当然,如果你曾经在低版本的 react 中使用过 Mixins,

  • React组件设计过程之仿抖音订单组件

    目录 前言 前期准备 实现后的组件效果 1. 组件设计思路 2. 实现 Myorder 组件 2.1 实现tab切换效果 2.2 获取数据 2.3 实现搜索功能 2.4 设置loading状态 2.5 实现Empty(空状态)组件 3. 实现 OederList 组件 4. 实现 OrderNote 组件 5. 实现 RecommendList 组件 最后 前言 作为数据驱动的领导者react/vue等MVVM框架的出现,帮我们减少了工作中大量的冗余代码, 一切皆组件的思想深得人心.组件就是对一

  • 浅谈React中组件间抽象

    关于今天要学习的组件间抽象其实我这小白看了几次还没弄明白,这次决定一探究竟.在组件构建中,通常有一类功能需要被不同的组件公用,此时就涉及抽象的概念,在React中我们主要了解mixin和高阶组件. mixin mixin的特性广泛存在于各个面向对象语言中,在ruby中,include关键词就是mixin,是将一个模块混入到另外一个模块中,或者是类中. 封装mixin方法 const mixin = function(obj, mixins) { const newObj = obj newObj

随机推荐