前端单页系统的权限控制
写在前面:说到权限的控制,前端的权限是不安全的。所有的权限应该都由后端来做才能保证安全。那么我们为什么要在前端做权限控制呢?答案很简单,为了用户体验。一个普通用户明明看到了一个路由,点击以后系统报错说没有权限的体验,远远不如用户一开始并没有看到这个路由。这就是前端做权限控制的意义。也就是说,前端只负责把有权限的页面或者按钮展示给用户,其他的隐藏起来。前端单页的应用大大提升了前端权限控制的需求。很多后台管理系统都是单页应用,也都需要权限控制,这样不同的角色可以进行不同的操作。我所做的项目也是如此。这里将react和react-router等相关的类容一起总结一下。
前端的权限的维度
经验统计,前端权限的体现主要有以下3种类型:
路由
最常见的就是菜单栏或者导航栏的控制,不同的用户看到的菜单栏不一样,比如登录某云的后台管理系统,发现导航栏有2个下拉的选项,而管理员登录却能看到十几二十个下拉选项。这样的权限,通常是针对整个页面的,也就是设计者希望没有权限的人整个页面都看不到。就算使用者知道页面的路由,进去以后依然什么也看不见。
按钮
同样的页面,可以操作的按钮不一样,比如登录某云的后台管理系统,一个工单,普通用户只看到“查看”按钮,但是管理员就能看到“查看”,“审批”两个按钮。这样的权限,通常是希望使用者能看到页面,但是页面上的某些操作不能操作。
请求
请求,是前端权限的最后一次拦截。就是即将要发出请求的时候对权限进行检验,如果没有权限则不发送这个请求。
以上3种形式组成了前端权限的控制。
实现思路
上面说到权限的3中体现,这里说明下针对这3种情况的实现思路
路由
初始化路由之前就拿到权限的列表,再进行初始化,直接根据权限,初始化相应的路由。
页面和按钮
后端对于实现权限、日志等功能都是借用AOP面向切面的思想来实现。面向切面的思想把一些与核心业务无关,但任何模块都可能使用的功能抽离出来,然后动态给业务模块添加上需要的功能。然鹅,前端并没有实现动态添加业务模块的机制,不过仍然可以参考这个思想。我们把与权限有关的抽离成一个公用的组件,在需要使用权限的地方初始化这个组件,这个组件的插入尽量做到不影响页面的原有逻辑。
请求
可以写一个公用的方法,这个方法可以判断,当前的用户是否有这个接口的访问权限,如果有就发送请求,如果没有就不发送请求。
实战
上面说了实现的思路,下面就具体说下怎么做。以react
+ react router
为例。
react路由权限控制
想要路由渲染的时候根据权限来进行渲染,那在初始化路由之前就应该拿到权限的列表。因此所有内容都应该在权限加载完以后加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import ReactDOM, { render } from 'react-dom' import React, { Component } from 'react' import App from 'es/components/app' import Permission from 'es/components/permission' import Root from './root' import { needPermissions as headPermissions } from '../../../permissions/es-header'
class MyAPP extends Component { render () { return ( <App> <Permission queryPermissions={headPermissions.concat(['ADMINGROUPS_GET_LIST'])} > <Root /> </Permission> </App> ); } }
render((<MyAPP />), document.getElementById('application'))
|
上面代码是页面的入口文件。可以看到,这个入口文件最外层组件是App
组件,其次是Permission
组件。 Root
组件就是页面真正的内容。为什么我们需要再最外层套两层呢?因为之前说的,我们在初始化路由的时候需要拿到用户的权限以及用户的信息,于是我们在App
组件获取到用户的信息,在Permission
组件中获取权限。这里可以根据各自的业务逻辑灵活使用,如果不需要用户的信息,可以直接只套一层。
在上面两个组件中获取到了用户和权限信息,我们怎么传递给其他组价使用呢?react中,传递变量通常是用props一层层往下传,但是像用户信息和权限这类信息,在全局任何地方都有可能用到,如果使用props从顶层组价一层层往下传会显得非常繁琐,debug问题的时候也很难快速定位。有没有什么方法可以让这类信息像全局变量一样,任何一个自组件都能轻松的访问到呢?有一个非官方的方法context
。
Context——定义游离于组件之外的全局变量
context的使用非常方便,只要在父组件或者说外层组件定义一个getChildContext
方法和childContextTypes
变量,在里层定义contextTypes
就可以直接在里层用this.context
访问。举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| import React, { Children, Component, ReactElement } from 'react'; import PropTypes from 'prop-types';
import request, { getApis } from 'es/utils/api-request'; import apis from 'es/constants/apis';
export default class Permission extends Component { _queryPermissions: Array<string>
static childContextTypes = { permissions: PropTypes.object, hasPermissions: PropTypes.func, hasSomePermissions: PropTypes.func }
getChildContext () { return { permissions: this.state.permissions, hasPermissions: this.hasPermissions.bind(this), hasSomePermissions: this.hasSomePermissions.bind(this) }; }
state = { permissions: undefined }
constructor (props, context) { super(props, context);
const {needPermissions = [], needSomePermissions = [], queryPermissions = [] } = this.props;
this._queryPermissions = Array.from( new Set([...queryPermissions, ...needPermissions, ...needSomePermissions])); }
componentDidMount () { this.getPermissions(); } hasPermissions (checkPermissions) { let { permissions = {} } = this.state; return !checkPermissions || checkPermissions.every((p) => permissions[p]); } hasSomePermissions (checkPermissions) { let { permissions = {} } = this.state; return !checkPermissions || checkPermissions.some((p) => permissions[p]); }
render () { let { props } = this; const {needPermissions, needSomePermissions, noPermissionChild } = props;
var child;
if (this.state.permissions) { if ( (needPermissions && !this.hasPermissions(needPermissions)) || (needSomePermissions && !this.hasSomePermissions(needSomePermissions)) ) { child = noPermissionChild; } else { child = props.children; } }
return child ? Children.only(child) : null; } }
|
Permission
组件的主要功能就是,从后端获取所有的permissions,放在自己的state中,并通过context,将permissions以及几个方法存储在全局变量中,判断传入的权限名该用户是不是有,没有就显示没有权限,有就显示子组件。
上面的组件和一般的组件没有什么差别,就是多定义了getChildContext
方法和childContextTypes
变量。
如何在子组件中拿到permissions呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Menu } from 'antd'; import { functionalModules, commonModules } from '../../../permissions/es-header';
export default class AppHeader extends Component { static contextTypes = { user: PropTypes.object, hasSomePermissions: PropTypes.func }
getUserName () { let { user } = this.context;
return ( <Menu.SubMenu title={(<span>{user.name}</span>)} > </Menu.SubMenu> ); }
getProducts (modules) { return modules.map((m) => ( this.checkAuth(m.apis) ? ( <Menu.Item key={m.text}> <a href={m.url}>{m.text}</a> </Menu.Item> ) : null )).filter((item) => !!item); }
checkAuth (apis) { let { hasSomePermissions } = this.context;
if (apis) { return hasSomePermissions(apis); } return hasSomePermissions(apis); }
render () { return ( <div> <Menu> <Menu.SubMenu title="功能1"> <Menu.ItemGroup title="具体模块"> {this.getProducts(functionalModules)} </Menu.ItemGroup> </Menu.SubMenu> </Menu> <Menu> {this.getUserName()} </Menu> </div> ) } }
|
如上代码,只要在组件中定义contextTypes
,就能this.context
直接调用需要的变量或者方法。
事实上,上面的代码就是路由权限控制的关键。我们把所有完整的路由以json的形式静态保存起来,Menu.ItemGroup
组件,渲染出来的就是根据用户的权限从完整路由中过滤出来的目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [ { "apis": [ "GET_USER_LIST", "ADD_USER_ITEM" ], "text": "用户管理", "url": "/user" }, { "apis": [ "ADD_FEE_ITEM", "GET_FEE_LIST" ], "text": "费用管理", "url": "/fee" } ]
|
这里实现的是一个很简单顶部。左侧导航栏也是同样的处理思路。
其实上还有一种思路是将路由的组件进行封装,这个组件在挂载前判断是不是有权限,就是利用路由的回调函数onEnter
,onLeave()
来做。这里没有那样做的理由是,如果按照这种思路来做,那每个路由的回调都会执行一次,有些浪费资源。
具体可以参看其他的文章:https://blog.csdn.net/qq_39985511/article/details/80885158
react页面和按钮权限控制
在路由的实现中其实已经实现了Permission
组件,无论是页面,还是按钮,只要需要权限控制的地方,就在组件外层添加这个组件,并传入需要的权限名字,如果有权限,就初始化子组件,没有就不处理或者显示没有权限。
一个页面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default class MyPageContainer extends Component { render () { return ( <div> <Breadcrumb routes={routes} params={allParams} /> <Permission needPermissions={['GET_LIST']} noPermissionChild={<span>无权限</span>} > <MyPage/> </Permission> </div> ) } }
|
一个按钮的例子:
1 2 3 4 5 6 7 8 9 10 11
| render () { return ( <div> <Permission needPermissions={['ADD_ITEM']} > <Button>新增</Button> </Permission> </div> ); }
|
发送请求时的权限
这个基于前面的内容,在发送请求的时候再进行一次判断。Permission
组件返回了一些方法,可以通过context来获取到。同样这些方法可以判断当前用户是不是拥有该接口的权限。如果拥有就发送请求,否则就不发送。我们可以将发送请求的方法提取出来一并处理。但是我们必须在前端维护一个接口和对应权限名称的文件。这样才能知道哪个接口需要的是什么权限。类似于下面这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "URSUSER_GET_LIST": { "url": "/users/info", "method": "GET", "action": "searchUserInfo", "resource": "/users/info" }, "URSUSER_MODIFY": { "url": "/users/modify", "method": "POST", "action": "modifyUserInfo", "resource": "/users/modify" } }
|
总结
有关权限的控制,都应该由后端来做。前端只是为了提高用户体验,让没有权限的资源不显示出来。前端的控制的主要思路就是有一个公用的方法,这个方法可以判断当前用户是不是拥有需要的权限,然后前端根据这个方法返回的结构生成路由,渲染页面等。
其他参考文章:
https://refined-x.com/2017/08/29/%E5%9F%BA%E4%BA%8EVue%E5%AE%9E%E7%8E%B0%E5%90%8E%E5%8F%B0%E7%B3%BB%E7%BB%9F%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6/