背景
在項(xiàng)目中要求在后臺系統(tǒng)控制管理權(quán)限。在之前做過的后臺管理系統(tǒng)權(quán)限控制是用Vue,這樣的話就可以用路由鉤子里做權(quán)限比對和攔截處理。但這次我們說的是在一個(gè)后臺系統(tǒng)需要加入權(quán)限管理控制,技術(shù)棧是React?,F(xiàn)在我們就看看實(shí)現(xiàn)過程吧。
原代碼基于 react 16.x、dva 2.4.1 實(shí)現(xiàn),所以本文是參考了ant-design-pro v1內(nèi)部對權(quán)限管理的實(shí)現(xiàn)
所謂的權(quán)限控制是什么?
一般后臺管理系統(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è)菜單的可見與否,并不是如此。簡單來說,當(dāng)用戶guest沒有路由/setting的訪問權(quán)限,這樣就可以知道/setting的完整路徑,就可以直接訪問進(jìn)入。這樣沒有任何作用啊。這部分其實(shí)就屬于路由層面的權(quán)限控制。
實(shí)現(xiàn)思路
關(guān)于前端權(quán)限控制一般有兩種方案:
前端固定路由表和權(quán)限配置,由后端提供用戶權(quán)限標(biāo)識
后端提供權(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í)是我從控制臺 copy 過來的,內(nèi)部做了很多的轉(zhuǎn)換處理,但最終生成的就是上面這個(gè)對象。
這里每一級菜單都加了一個(gè)authority字段來標(biāo)識允許訪問的角色。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({</p> <p> app,</p> <p> ...dynamics</p> <p> })} /> ))} <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)前菜單。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/127834.html
摘要:在的的配置中添加自定義主題由腳手架和官網(wǎng)介紹,我們已經(jīng)自己配置并新建好了主題文件。單頁面博客從前端到后端環(huán)境搭建單頁面博客從前端到后端基于搭建博客前后臺界面單頁面博客從前端到后端基于和的權(quán)限驗(yàn)證與的設(shè)計(jì) 在上篇文章我們已經(jīng)搭建好了基礎(chǔ)的開發(fā)環(huán)境,接下來會(huì)介紹如何引入 DVA 和 ANTD ,以及在引入過程中需要注意的問題。這里只會(huì)詳細(xì)的書寫部分組件,其他的組件都是大同小異。你可以在 g...
摘要:面試問到的問題繼承的幾種方法,,原形繼承面向?qū)ο蟮膸追N方法五種方式對象字面量創(chuàng)建實(shí)例對象構(gòu)造函數(shù)工廠模式用一個(gè)函數(shù),通過傳遞參數(shù)返回對象。打包原理打包原理把所有依賴打包成一個(gè)文件,通過代碼分割成單元片段并按需加載。 面試問到的問題:1、繼承的幾種方法; Call,apply,原形繼承; 2、面向?qū)ο蟮膸追N方法; 五種方式: 1)對象字面量:var obj={}; 2)創(chuàng)建實(shí)例對象:va...
摘要:面試問到的問題繼承的幾種方法,,原形繼承面向?qū)ο蟮膸追N方法五種方式對象字面量創(chuàng)建實(shí)例對象構(gòu)造函數(shù)工廠模式用一個(gè)函數(shù),通過傳遞參數(shù)返回對象。打包原理打包原理把所有依賴打包成一個(gè)文件,通過代碼分割成單元片段并按需加載。 面試問到的問題:1、繼承的幾種方法; Call,apply,原形繼承; 2、面向?qū)ο蟮膸追N方法; 五種方式: 1)對象字面量:var obj={}; 2)創(chuàng)建實(shí)例對象:va...
摘要:面試問到的問題繼承的幾種方法,,原形繼承面向?qū)ο蟮膸追N方法五種方式對象字面量創(chuàng)建實(shí)例對象構(gòu)造函數(shù)工廠模式用一個(gè)函數(shù),通過傳遞參數(shù)返回對象。打包原理打包原理把所有依賴打包成一個(gè)文件,通過代碼分割成單元片段并按需加載。 面試問到的問題:1、繼承的幾種方法; Call,apply,原形繼承; 2、面向?qū)ο蟮膸追N方法; 五種方式: 1)對象字面量:var obj={}; 2)創(chuàng)建實(shí)例對象:va...
摘要:前言以前一直是用進(jìn)行的開發(fā)于是決定年后弄一弄所以年后這段時(shí)間也就一直瞎弄可算是看到成果了本來是想寫一個(gè)類似仿今日頭條那樣的項(xiàng)目來入手后來又尋思還不如寫個(gè)后臺管理呢。于是乎自己便著手簡單的搭建了一個(gè)集中設(shè)置的版本。 前言 以前一直是用vue進(jìn)行的開發(fā), 于是決定年后弄一弄react, 所以年后這段時(shí)間也就一直瞎弄react, 可算是看到成果了 本來是想寫一個(gè) 類似 Vue仿今日頭條 那樣...
閱讀 596·2023-03-27 18:33
閱讀 790·2023-03-26 17:27
閱讀 684·2023-03-26 17:14
閱讀 645·2023-03-17 21:13
閱讀 573·2023-03-17 08:28
閱讀 1895·2023-02-27 22:32
閱讀 1377·2023-02-27 22:27
閱讀 2280·2023-01-20 08:28