权限管理是中后台系统中常见的需求之一。之前做过基于 Vue 的后台管理系统权限控制[1],基本思路就是在一些路由钩子里做权限比对和拦截处理。
最近维护的一个后台系统需要加入权限管理控制,这次技术栈是React
,我刚开始是在网上搜索一些React路由权限控制
,但是没找到比较好的方案或思路。
这时想到ant design pro
内部实现过权限管理,因此就专门花时间翻阅了一波源码,并在此基础上逐渐完成了这次的权限管理。
整个过程也是遇到了很多问题,本文主要来做一下此次改造工作的总结。
原代码基于 react 16.x、dva 2.4.1 实现,所以本文是参考了ant-design-pro v1[2]内部对权限管理的实现
一般后台管理系统的权限涉及到两种:
资源权限一般指菜单、页面、按钮等的可见权限。
数据权限一般指对于不同用户,同一页面上看到的数据不同。
本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:
在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户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
方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。
[1]基于 Vue 的后台管理系统权限控制: https://github.com/easy-wheel/ts-vue/blob/master/src/peimission.ts
[2]ant-design-pro v1: https://github.com/ant-design/ant-design-pro/tree/v1
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/yPNRbCunoAjNwPD472nXFg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。