所谓的权限控制是什么?
一般后台管理系统的权限涉及到两种:
资源权限一般指菜单、页面、按钮等的可见权限。
数据权限一般指对于不同用户,同一页面上看到的数据不同。
本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:
在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户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 modelsmodels.forEach(model => {if (modelNotExisted(app, model)) {// eslint-disable-next-lineapp.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 || rawreturn props =>createElement(Component, {...props,routerData: getRouterDataCache(app)})})},// 全局loadingloading: () => {return (<divstyle={{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) => (<Routekey={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["/"].componentreturn (<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.propsconst childrenRender = typeof children === "undefined" ? null : childrenreturn 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.propsreturn (<Authorizedauthority={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 (<SubMenutitle={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 domconst 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.propsif (Authorized && Authorized.check) {const { check } = Authorizedreturn check(authority, ItemDom)}return ItemDom}render() {// ...return<Sidertrigger={null}collapsiblecollapsed={collapsed}breakpoint="lg"onCollapse={onCollapse}className={siderClass}><div className="logo"><Link to="/home" className="logo-link">{!collapsed && <h1>冯言冯语</h1>}</Link></div><Menukey="Menu"theme={theme}mode={mode}{...menuProps}onOpenChange={this.handleOpenChange}selectedKeys={selectedKeys}>{this.getNavMenuItems(menuData)}</Menu></Sider>}
}
这里我只贴了一些核心代码,其中的checkPermissionItem 就是实现菜单权限的关键。他同样用到了上文中的check 方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享
|