React中的权限组件设计问题怎么解决
导读:本文共7213.5字符,通常情况下阅读需要24分钟。同时您也可以点击右侧朗读,来听本文内容。按键盘←(左) →(右) 方向键可以翻页。
摘要: 背景权限管理是中后台系统中常见的需求之一。之前做过基于 Vue 的后台管理系统权限控制,基本思路就是在一些路由钩子里做权限比对和拦截处理。最近维护的一个后台系统需要加入权限管理控制,这次技术栈是React,我刚开始是在网上搜索一些React路由权限控制,但是没找到比较好的方案或思路。这时想到ant design pro内部实现过权限管理,因此就专门花时间翻阅了一... ...
目录
(为您整理了一些要点),点击可以直达。背景
权限管理是中后台系统中常见的需求之一。之前做过基于 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
代表路由对应的组件:
importReact,{createElement}from"react"importLoadablefrom"react-loadable""/activity-mgmt/list":{component:dynamicWrapper(app,["activityMgmt"],()=>import("../routes/activity-mgmt/list"))},//动态引用组件并注册modelconstdynamicWrapper=(app,models,component)=>{//registermodelsmodels.forEach(model=>{if(modelNotExisted(app,model)){//eslint-disable-next-lineapp.model(require(`../models/${model}`).default)}})//()=>require('module')//transformedbybabel-plugin-dynamic-import-node-sync//需要将routerData塞到props中if(component.toString().indexOf(".then(")<0){returnprops=>{returncreateElement(component().default,{...props,routerData:getRouterDataCache(app)})}}//()=>import('module')returnLoadable({loader:()=>{returncomponent().then(raw=>{constComponent=raw.default||rawreturnprops=>createElement(Component,{...props,routerData:getRouterDataCache(app)})})},//全局loadingloading:()=>{return(<divstyle={{display:"flex",justifyContent:"center",alignItems:"center"}}><Spinsize="large"className="global-spin"/></div>)}})}
有了路由表这份基础数据,下面就让我们来看下如何通过一步步的改造给原有系统注入权限。
先从src/router.js
这个入口开始着手:
//原src/router.jsimportdynamicfrom"dva/dynamic"import{Redirect,Route,routerRedux,Switch}from"dva/router"importPropTypesfrom"prop-types"importReactfrom"react"importNoMatchfrom"./components/no-match"importAppfrom"./routes/app"const{ConnectedRouter}=routerReduxconstRouterConfig=({history,app})=>{constroutes=[{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(<ConnectedRouterhistory={history}><App><Switch>{routes.map(({path,...dynamics},key)=>(<Routekey={key}path={`/${path}`}component={dynamic({app,...dynamics})}/>))}<Routecomponent={NoMatch}/></Switch></App></ConnectedRouter>)}RouterConfig.propTypes={history:PropTypes.object,app:PropTypes.object}exportdefaultRouterConfig
这是一个非常常规的路由配置,既然要加入权限,比较合适的方式就是包一个高阶组件AuthorizedRoute
。然后router.js
就可以更替为:
functionRouterConfig({history,app}){constrouterData=getRouterData(app)constBasicLayout=routerData["/"].componentreturn(<ConnectedRouterhistory={history}><Switch><AuthorizedRoutepath="/"render={props=><BasicLayout{...props}/>}/></Switch></ConnectedRouter>)}
来看下AuthorizedRoute
的大致实现:
constAuthorizedRoute=({component:Component,authority,redirectPath,{...rest}})=>{if(authority===currentAuthority){return(<Route{...rest}render={props=><Component{...props}/>}/>)}else{return(<Route{...rest}render={()=><Redirectto={redirectPath}/>}/>)}}
我们看一下这个组件有什么问题:页面可能允许多个角色访问,用户拥有的角色也可能是多个(可能是字符串,也可呢是数组)。
直接在组件中判断显然不太合适,我们把这部分逻辑抽离出来:
/***通用权限检查方法*Commoncheckpermissionsmethod*@param{菜单访问需要的权限}authority*@param{当前角色拥有的权限}currentAuthority*@param{通过的组件Passingcomponents}target*@param{未通过的组件nopasscomponents}Exception*/constcheckPermissions=(authority,currentAuthority,target,Exception)=>{console.log("checkPermissions----->authority",authority)console.log("currentAuthority",currentAuthority)console.log("target",target)console.log("Exception",Exception)//没有判定权限.默认查看所有//Retirementauthority,returntarget;if(!authority){returntarget}//数组处理if(Array.isArray(authority)){//该菜单可由多个角色访问if(authority.indexOf(currentAuthority)>=0){returntarget}//当前用户同时拥有多个角色if(Array.isArray(currentAuthority)){for(leti=0;i<currentAuthority.length;i+=1){constelement=currentAuthority[i]//菜单访问需要的角色权限<------>当前用户拥有的角色if(authority.indexOf(element)>=0){returntarget}}}returnException}//string处理if(typeofauthority==="string"){if(authority===currentAuthority){returntarget}if(Array.isArray(currentAuthority)){for(leti=0;i<currentAuthority.length;i+=1){constelement=currentAuthority[i]if(authority.indexOf(element)>=0){returntarget}}}returnException}thrownewError("unsupportedparameters")}constcheck=(authority,target,Exception)=>{returncheckPermissions(authority,CURRENT,target,Exception)}
首先如果路由表中没有authority
字段默认都可以访问。
接着分别对authority
为字符串和数组的情况做了处理,其实就是简单的查找匹配,匹配到了就可以访问,匹配不到就返回Exception
,也就是我们自定义的异常页面。
有一个点一直没有提:用户当前角色权限
currentAuthority
如何获取?这个是在页面初始化时从接口读取,然后存到store
中
有了这块逻辑,我们对刚刚的AuthorizedRoute
做一下改造。首先抽象一个Authorized
组件,对权限校验逻辑做一下封装:
importReactfrom"react"importCheckPermissionsfrom"./CheckPermissions"classAuthorizedextendsReact.Component{render(){const{children,authority,noMatch=null}=this.propsconstchildrenRender=typeofchildren==="undefined"?null:childrenreturnCheckPermissions(authority,childrenRender,noMatch)}}exportdefaultAuthorized
接着AuthorizedRoute
可直接使用Authorized
组件:
importReactfrom"react"import{Redirect,Route}from"react-router-dom"importAuthorizedfrom"./Authorized"classAuthorizedRouteextendsReact.Component{render(){const{component:Component,render,authority,redirectPath,...rest}=this.propsreturn(<Authorizedauthority={authority}noMatch={<Route{...rest}render={()=><Redirectto={{pathname:redirectPath}}/>}/>}><Route{...rest}render={props=>(Component?<Component{...props}/>:render(props))}/></Authorized>)}}exportdefaultAuthorizedRoute
这里采用了render props
的方式:如果提供了component props
就用component
渲染,否则使用render
渲染。
菜单权限
菜单权限的处理相对就简单很多了,统一集成到SiderMenu
组件处理:
exportdefaultclassSiderMenuextendsPureComponent{constructor(props){super(props)}/***getSubMenuorItem*/getSubMenuOrItem=item=>{if(item.children&&item.children.some(child=>child.name)){constchildrenItems=this.getNavMenuItems(item.children)//当无子菜单时就不展示菜单if(childrenItems&&childrenItems.length>0){return(<SubMenutitle={item.icon?(<span>{getIcon(item.icon)}<span>{item.name}</span></span>):(item.name)}key={item.path}>{childrenItems}</SubMenu>)}returnnull}return<Menu.Itemkey={item.path}>{this.getMenuItemPath(item)}</Menu.Item>}/***获得菜单子节点*@memberofSiderMenu*/getNavMenuItems=menusData=>{if(!menusData){return[]}returnmenusData.filter(item=>item.name&&!item.hideInMenu).map(item=>{//makedomconstItemDom=this.getSubMenuOrItem(item)returnthis.checkPermissionItem(item.authority,ItemDom)}).filter(item=>item)}/****@description菜单权限过滤*@param{*}authority*@param{*}ItemDom*@memberofSiderMenu*/checkPermissionItem=(authority,ItemDom)=>{const{Authorized}=this.propsif(Authorized&&Authorized.check){const{check}=Authorizedreturncheck(authority,ItemDom)}returnItemDom}render(){//...return<Sidertrigger={null}collapsiblecollapsed={collapsed}breakpoint="lg"onCollapse={onCollapse}className={siderClass}><divclassName="logo"><Linkto="/home"className="logo-link">{!collapsed&&<h2>冯言冯语</h2>}</Link></div><Menukey="Menu"theme={theme}mode={mode}{...menuProps}onOpenChange={this.handleOpenChange}selectedKeys={selectedKeys}>{this.getNavMenuItems(menuData)}</Menu></Sider>}}
这里我只贴了一些核心代码,其中的checkPermissionItem
就是实现菜单权限的关键。他同样用到了上文中的check
方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。
</div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
React中的权限组件设计问题怎么解决的详细内容,希望对您有所帮助,信息来源于网络。