<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
許可權管理是中後臺系統中常見的需求之一。之前做過基於 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許可權元件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45