React中的權(quán)限組件設(shè)計(jì)問題小結(jié)
背景
權(quán)限管理是中后臺(tái)系統(tǒng)中常見的需求之一。之前做過基于 Vue 的后臺(tái)管理系統(tǒng)權(quán)限控制,基本思路就是在一些路由鉤子里做權(quán)限比對和攔截處理。
最近維護(hù)的一個(gè)后臺(tái)系統(tǒng)需要加入權(quán)限管理控制,這次技術(shù)棧是React,我剛開始是在網(wǎng)上搜索一些React路由權(quán)限控制,但是沒找到比較好的方案或思路。
這時(shí)想到ant design pro內(nèi)部實(shí)現(xiàn)過權(quán)限管理,因此就專門花時(shí)間翻閱了一波源碼,并在此基礎(chǔ)上逐漸完成了這次的權(quán)限管理。
整個(gè)過程也是遇到了很多問題,本文主要來做一下此次改造工作的總結(jié)。
原代碼基于 react 16.x、dva 2.4.1 實(shí)現(xiàn),所以本文是參考了ant-design-pro v1內(nèi)部對權(quán)限管理的實(shí)現(xiàn)
所謂的權(quán)限控制是什么?
一般后臺(tái)管理系統(tǒng)的權(quán)限涉及到兩種:
- 資源權(quán)限
- 數(shù)據(jù)權(quán)限
資源權(quán)限一般指菜單、頁面、按鈕等的可見權(quán)限。
數(shù)據(jù)權(quán)限一般指對于不同用戶,同一頁面上看到的數(shù)據(jù)不同。
本文主要是來探討一下資源權(quán)限,也就是前端權(quán)限控制。這又分為了兩部分:
- 側(cè)邊欄菜單
- 路由權(quán)限
在很多人的理解中,前端權(quán)限控制就是左側(cè)菜單的可見與否,其實(shí)這是不對的。舉一個(gè)例子,假設(shè)用戶guest沒有路由/setting的訪問權(quán)限,但是他知道/setting的完整路徑,直接通過輸入路徑的方式訪問,此時(shí)仍然是可以訪問的。這顯然是不合理的。這部分其實(shí)就屬于路由層面的權(quán)限控制。
實(shí)現(xiàn)思路
關(guān)于前端權(quán)限控制一般有兩種方案:
- 前端固定路由表和權(quán)限配置,由后端提供用戶權(quán)限標(biāo)識(shí)
- 后端提供權(quán)限和路由信息結(jié)構(gòu)接口,動(dòng)態(tài)生成權(quán)限和菜單
我們這里采用的是第一種方案,服務(wù)只下發(fā)當(dāng)前用戶擁有的角色就可以了,路由表和權(quán)限的處理統(tǒng)一在前端處理。
整體實(shí)現(xiàn)思路也比較簡單:現(xiàn)有權(quán)限(currentAuthority)和準(zhǔn)入權(quán)限(authority)做比較,如果匹配則渲染和準(zhǔn)入權(quán)限匹配的組件,否則渲染無權(quán)限組件(403 頁面)

路由權(quán)限
既然是路由相關(guān)的權(quán)限控制,我們免不了先看一下當(dāng)前的路由表:
{
"name": "活動(dòng)列表",
"path": "/activity-mgmt/list",
"key": "/activity-mgmt/list",
"exact": true,
"authority": [
"admin"
],
"component": ? LoadableComponent(props),
"inherited": false,
"hideInBreadcrumb": false
},
{
"name": "優(yōu)惠券管理",
"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": "營銷錄入系統(tǒng)",
"path": "/marketRule-manage",
"key": "/marketRule-manage",
"exact": true,
"component": ? LoadableComponent(props),
"inherited": true,
"hideInBreadcrumb": false
}這份路由表其實(shí)是我從控制臺(tái) copy 過來的,內(nèi)部做了很多的轉(zhuǎn)換處理,但最終生成的就是上面這個(gè)對象。
這里每一級菜單都加了一個(gè)authority字段來標(biāo)識(shí)允許訪問的角色。component代表路由對應(yīng)的組件:
import React, { createElement } from "react"
import Loadable from "react-loadable"
"/activity-mgmt/list": {
component: dynamicWrapper(app, ["activityMgmt"], () => import("../routes/activity-mgmt/list"))
},
// 動(dòng)態(tài)引用組件并注冊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>
)
}
})
}
復(fù)制代碼有了路由表這份基礎(chǔ)數(shù)據(jù),下面就讓我們來看下如何通過一步步的改造給原有系統(tǒng)注入權(quán)限。
先從src/router.js這個(gè)入口開始著手:
// 原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這是一個(gè)非常常規(guī)的路由配置,既然要加入權(quán)限,比較合適的方式就是包一個(gè)高階組件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的大致實(shí)現(xiàn):
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} />
} />
)
}
}我們看一下這個(gè)組件有什么問題:頁面可能允許多個(gè)角色訪問,用戶擁有的角色也可能是多個(gè)(可能是字符串,也可呢是數(shù)組)。
直接在組件中判斷顯然不太合適,我們把這部分邏輯抽離出來:
/**
* 通用權(quán)限檢查方法
* Common check permissions method
* @param { 菜單訪問需要的權(quán)限 } authority
* @param { 當(dāng)前角色擁有的權(quán)限 } 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)
// 沒有判定權(quán)限.默認(rèn)查看所有
// Retirement authority, return target;
if (!authority) {
return target
}
// 數(shù)組處理
if (Array.isArray(authority)) {
// 該菜單可由多個(gè)角色訪問
if (authority.indexOf(currentAuthority) >= 0) {
return target
}
// 當(dāng)前用戶同時(shí)擁有多個(gè)角色
if (Array.isArray(currentAuthority)) {
for (let i = 0; i < currentAuthority.length; i += 1) {
const element = currentAuthority[i]
// 菜單訪問需要的角色權(quán)限 < ------ > 當(dāng)前用戶擁有的角色
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字段默認(rèn)都可以訪問。
接著分別對authority為字符串和數(shù)組的情況做了處理,其實(shí)就是簡單的查找匹配,匹配到了就可以訪問,匹配不到就返回Exception,也就是我們自定義的異常頁面。
有一個(gè)點(diǎn)一直沒有提:用戶當(dāng)前角色權(quán)限
currentAuthority如何獲???這個(gè)是在頁面初始化時(shí)從接口讀取,然后存到store中
有了這塊邏輯,我們對剛剛的AuthorizedRoute做一下改造。首先抽象一個(gè)Authorized組件,對權(quán)限校驗(yàn)邏輯做一下封裝:
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渲染。
菜單權(quán)限
菜單權(quán)限的處理相對就簡單很多了,統(tǒng)一集成到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)
// 當(dāng)無子菜單時(shí)就不展示菜單
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>
}
/**
* 獲得菜單子節(jié)點(diǎn)
* @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 菜單權(quán)限過濾
* @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就是實(shí)現(xiàn)菜單權(quán)限的關(guān)鍵。他同樣用到了上文中的check方法來對當(dāng)前菜單進(jìn)行權(quán)限比對,如果沒有權(quán)限就直接不展示當(dāng)前菜單。
到此這篇關(guān)于React中的權(quán)限組件設(shè)計(jì)的文章就介紹到這了,更多相關(guān)React權(quán)限組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用React高階組件實(shí)現(xiàn)一個(gè)面包屑導(dǎo)航的示例
這篇文章主要介紹了利用React高階組件實(shí)現(xiàn)一個(gè)面包屑導(dǎo)航的示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
React項(xiàng)目中className運(yùn)用及問題解決
這篇文章主要為大家介紹了React項(xiàng)目中className運(yùn)用及問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
React Hook useState useEffect componentD
這篇文章主要介紹了React Hook useState useEffect componentDidMount componentDidUpdate componentWillUnmount問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
一文詳解ReactNative狀態(tài)管理redux-toolkit使用
這篇文章主要為大家介紹了ReactNative狀態(tài)管理redux-toolkit使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
解決React報(bào)錯(cuò)Cannot?find?namespace?context
這篇文章主要為大家介紹了React報(bào)錯(cuò)Cannot?find?namespace?context分析解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
React自定義視頻全屏按鈕實(shí)現(xiàn)全屏功能
這篇文章主要介紹了React自定義視頻全屏按鈕實(shí)現(xiàn)全屏功能,通過繪制全屏按鈕,并綁定點(diǎn)擊事件,編寫點(diǎn)擊事件,通過實(shí)例代碼給大家詳細(xì)講解,需要的朋友可以參考下2022-11-11
React實(shí)現(xiàn)單向數(shù)據(jù)流的方法
本文主要介紹了React實(shí)現(xiàn)單向數(shù)據(jù)流的方法2023-04-04

