聊聊React中的权限组件设计

所谓的权限控制是什么?

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

  • 资源权限
  • 数据权限

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

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

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

  • 侧边栏菜单
  • 路由权限

在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户guest没有路由/setting的访问权限,但是他知道/setting的完整路径,直接通过输入路径的方式访问,此时仍然是可以访问的。这显然是不合理的。这部分其实就属于路由层面的权限控制。

实现思路

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

  • 前端固定路由表和权限配置,由后端提供用户权限标识
  • 后端提供权限和路由信息结构接口,动态生成权限和菜单

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

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

聊聊React中的权限组件设计_第1张图片

路由权限

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

{"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 modelsmodels.forEach(model => {if (modelNotExisted(app, model)) {// eslint-disable-next-lineapp.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 || rawreturn props =>createElement(Component, {...props,routerData: getRouterDataCache(app)})})},// 全局loadingloading: () => {return (
)}}) }

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

先从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 ({routes.map(({ path, ...dynamics }, key) => ())})
}

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["/"].componentreturn ( } />)
} 

来看下AuthorizedRoute的大致实现:

const AuthorizedRoute = ({component: Component,authority,redirectPath,{...rest}
}) => {if (authority === currentAuthority) {return ( } />)} else {return (} />)}
} 

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

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

/**
 * 通用权限检查方法
 * 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.propsconst childrenRender = typeof children === "undefined" ? null : childrenreturn 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.propsreturn ( } />}> (Component ?  : render(props))} />)}
}

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 ({getIcon(item.icon)}{item.name}) : (item.name)}key={item.path}>{childrenItems})}return null}return {this.getMenuItemPath(item)}}/** * 获得菜单子节点 * @memberof SiderMenu */getNavMenuItems = menusData => {if (!menusData) {return []}return menusData.filter(item => item.name && !item.hideInMenu).map(item => {// make domconst 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.propsif (Authorized && Authorized.check) {const { check } = Authorizedreturn check(authority, ItemDom)}return ItemDom}render() {// ...return
{!collapsed &&

冯言冯语

}
{this.getNavMenuItems(menuData)}} }

这里我只贴了一些核心代码,其中的checkPermissionItem就是实现菜单权限的关键。他同样用到了上文中的check方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

你可能感兴趣的:(react.js,前端,javascript)