Open worldzhao opened 3 years ago
前端进行权限控制只是为了用户体验,对应的角色渲染对应的视图,真正的安全保障在后端。
毕业之初,工作的主要内容便是开发一个后台管理系统,当时存在的一个现象是:
用户若记住了某个 url,直接浏览器输入,不论该用户是否拥有访问该页面的权限,均能进入页面。
若页面初始化时(componentDidMount)进行接口请求,后端会返回 403 的 HTTP 状态码,同时前端封装的request.js会对非业务异常进行相关处理,遇见 403,就重定向到无权限页面。
componentDidMount
request.js
若是页面初始化时不存在前后端交互,那就要等用户触发某些操作(比如表单提交)后才会触发上述流程。
可以看到,安全保障是后端兜底的,那前端能做些什么呢?
最近也在看Ant Design Pro的权限相关处理,有必要进行一次总结。
Ant Design Pro
需要注意的是,本文虽然基于Ant Design Pro的权限设计思路,但并不是完全对其源码的解读(可能更偏向于 v1 的涉及思路,不涉及 umi)。
如果有错误以及理解偏差请轻捶并指正,谢谢。
假设存在以下关系:
某页面上存在一个文案为“进入管理后台”的按钮,只对管理员展示,让我们实现一下。
// currentAuthority 为当前用户权限枚举值 const AdminBtn = ({ currentAuthority }) => { if ('admin' === currentAuthority) { return <button>进入管理后台</button>; } return null; };
好吧,简单至极。
权限控制就是if else,实现功能并不复杂,大不了每个页面|模块|按钮涉及到的处理都写一遍判断就是了,总能实现需求的。
if else
不过,现在只是一个页面中的一个按钮而已,我们还会碰到许多“某(几)个页面存在某个 xxx,只对 xxx(或/以及 xxx) 展示”的场景。
所以,还能做的更好一些。
下面来封装一个最基本的权限管理组件Authorized。
Authorized
期望调用形式如下:
<Authorized currentAuthority={currentAuthority} authority={'admin'} noMatch={null}> <button>进入管理后台</button> </Authorized>
api 如下:
currentAuthority这个属性没有必要每次调用都手动传递一遍,此处假设用户信息是通过 redux 获取并存放在全局 store 中。
currentAuthority
redux
store
注意:我们当然也可以将用户信息挂在 window 下或者 localStorage 中,但很重要的一点是,绝大部分场景我们都是通过接口异步获取的数据,这点至关重要。如果是 html 托管在后端或是 ssr的情况下,服务端直接注入了用户信息,那真是再好不过了。
window
localStorage
html
ssr
新建src/components/Authorized/Authorized.jsx实现如下:
src/components/Authorized/Authorized.jsx
import { connect } from 'react-redux'; function Authorized(props) { const { children, userInfo, authority, noMatch } = props; const { currentAuthority } = userInfo || {}; if (!authority) return children; const _authority = Array.isArray(authority) ? authority : [authority]; if (_authority.includes(currentAuthority)) return children; return noMatch; } export default connect(store => ({ userInfo: store.common.userInfo }))(Authorized);
现在我们无需手动传递currentAuthority:
<Authorized authority={'admin'} noMatch={null}> <button>进入管理后台</button> </Authorized>
✨ 很好,我们现在迈出了第一步。
在Ant Design Pro中,对于currentAuthority(当前权限)与authority(准入权限)的匹配功能,定义了一个checkPermissions方法,提供了各种形式的匹配,本文只讨论authority为数组(多个准入权限)或字符串(单个准入权限),currentAuthority为字符串(当前角色只有一种权限)的情况。
authority
checkPermissions
页面就是放在Route组件下的模块。
Route
知道这一点后,我们很轻松的可以写出如下代码:
新建src/router/index.jsx,当用户角色与路由不匹配时,渲染Redirect组件用于重定向。
src/router/index.jsx
Redirect
import React from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import NormalPage from '@/views/NormalPage'; /* 公开页面 */ import UserPage from '@/views/UserPage'; /* 普通用户和管理员均可访问的页面*/ import AdminPage from '@/views/AdminPage'; /* 管理员才可访问的页面*/ import Authorized from '@/components/Authorized'; // Layout就是一个布局组件,写一些公用头部底部啥的 function Router() { return ( <BrowserRouter> <Layout> <Switch> <Route exact path='/' component={NormalPage} /> <Authorized authority={['admin', 'user']} noMatch={ <Route path='/user-page' render={() => <Redirect to={{ pathname: '/login' }} />} /> } > <Route path='/user-page' component={UserPage} /> </Authorized> <Authorized authority={'admin'} noMatch={ <Route path='/admin-page' render={() => <Redirect to={{ pathname: '/403' }} />} /> } > <Route path='/admin-page' component={AdminPage} /> </Authorized> </Switch> </Layout> </BrowserRouter> ); } export default Router;
这段代码是不 work 的,因为当前权限信息是通过接口异步获取的,此时Authorized组件获取不到当前权限(currentAuthority),倘若直接通过 url 访问/user-page或/admin-page,不论用户身份是否符合,请求结果未回来,都会被重定向到/login或/403,这个问题后面再谈。
/user-page
/admin-page
/login
/403
先优化一下我们的代码。
路由配置相关 jsx 内容太多了,页面数量过多就不好维护了,可读性也大大降低,我们可以将路由配置抽离出来。
新建src/router/router.config.js,专门用于存放路由相关配置信息。
src/router/router.config.js
import NormalPage from '@/views/NormalPage'; import UserPage from '@/views/UserPage'; import AdminPage from '@/views/AdminPage'; export default [ { exact: true, path: '/', component: NormalPage, }, { path: '/user-page', component: UserPage, authority: ['user', 'admin'], redirectPath: '/login', }, { path: '/admin-page', component: AdminPage, authority: ['admin'], redirectPath: '/403', }, ];
接下来基于Authorized组件对Route组件进行二次封装。
新建src/components/Authorized/AuthorizedRoute.jsx。
src/components/Authorized/AuthorizedRoute.jsx
实现如下:
import React from 'react'; import { Route } from 'react-router-dom'; import Authorized from './Authorized'; function AuthorizedRoute({ component: Component, render, authority, redirectPath, ...rest }) { 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;
现在重写我们的 Router 组件。
import React from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import AuthorizedRoute from '@/components/AuthorizedRoute'; import routeConfig from './router.config.js'; function Router() { return ( <BrowserRouter> <Layout> <Switch> {routeConfig.map(rc => { const { path, component, authority, redirectPath, ...rest } = rc; return ( <AuthorizedRoute key={path} path={path} component={component} authority={authority} redirectPath={redirectPath} {...rest} /> ); })} </Switch> </Layout> </BrowserRouter> ); } export default Router;
心情舒畅了许多。
可是还留着一个问题呢——由于用户权限信息是异步获取的,在权限信息数据返回之前,AuthorizedRoute组件就将用户推到了redirectPath。
AuthorizedRoute
redirectPath
其实Ant Design Pro v4 版本就有存在这个问题,相较于 v2 的@/pages/Authorized组件从localStorage中获取权限信息,v4 改为从 redux 中获取(redux 中的数据则是通过接口获取),和本文比较类似。具体可见此次 PR。
@/pages/Authorized
解决思路很简单:保证相关权限组件挂载时,redux 中已经存在用户权限信息。换句话说,接口数据返回后,再进行相关渲染。
我们可以在 Layout 中进行用户信息的获取,数据获取完毕后渲染children。
children
如果有许多组件都需要相同的功能的话,就可以使用HOC模式
没错,只用 HOC 可以更方便的定制相关的 fallback 逻辑,而配置化路由扩展起来就会耦合性过高,可以按需选择
前端进行权限控制只是为了用户体验,对应的角色渲染对应的视图,真正的安全保障在后端。
前言
毕业之初,工作的主要内容便是开发一个后台管理系统,当时存在的一个现象是:
用户若记住了某个 url,直接浏览器输入,不论该用户是否拥有访问该页面的权限,均能进入页面。
若页面初始化时(
componentDidMount
)进行接口请求,后端会返回 403 的 HTTP 状态码,同时前端封装的request.js
会对非业务异常进行相关处理,遇见 403,就重定向到无权限页面。若是页面初始化时不存在前后端交互,那就要等用户触发某些操作(比如表单提交)后才会触发上述流程。
可以看到,安全保障是后端兜底的,那前端能做些什么呢?
最近也在看
Ant Design Pro
的权限相关处理,有必要进行一次总结。需要注意的是,本文虽然基于
Ant Design Pro
的权限设计思路,但并不是完全对其源码的解读(可能更偏向于 v1 的涉及思路,不涉及 umi)。如果有错误以及理解偏差请轻捶并指正,谢谢。
模块级别的权限处理
假设存在以下关系:
某页面上存在一个文案为“进入管理后台”的按钮,只对管理员展示,让我们实现一下。
简单实现
好吧,简单至极。
权限控制就是
if else
,实现功能并不复杂,大不了每个页面|模块|按钮涉及到的处理都写一遍判断就是了,总能实现需求的。不过,现在只是一个页面中的一个按钮而已,我们还会碰到许多“某(几)个页面存在某个 xxx,只对 xxx(或/以及 xxx) 展示”的场景。
所以,还能做的更好一些。
下面来封装一个最基本的权限管理组件
Authorized
。组件封装-Authorized
期望调用形式如下:
api 如下:
currentAuthority
这个属性没有必要每次调用都手动传递一遍,此处假设用户信息是通过redux
获取并存放在全局store
中。注意:我们当然也可以将用户信息挂在
window
下或者localStorage
中,但很重要的一点是,绝大部分场景我们都是通过接口异步获取的数据,这点至关重要。如果是html
托管在后端或是ssr
的情况下,服务端直接注入了用户信息,那真是再好不过了。新建
src/components/Authorized/Authorized.jsx
实现如下:现在我们无需手动传递
currentAuthority
:✨ 很好,我们现在迈出了第一步。
页面级别的权限处理
页面就是放在
Route
组件下的模块。知道这一点后,我们很轻松的可以写出如下代码:
新建
src/router/index.jsx
,当用户角色与路由不匹配时,渲染Redirect
组件用于重定向。这段代码是不 work 的,因为当前权限信息是通过接口异步获取的,此时
Authorized
组件获取不到当前权限(currentAuthority
),倘若直接通过 url 访问/user-page
或/admin-page
,不论用户身份是否符合,请求结果未回来,都会被重定向到/login
或/403
,这个问题后面再谈。先优化一下我们的代码。
抽离路由配置
路由配置相关 jsx 内容太多了,页面数量过多就不好维护了,可读性也大大降低,我们可以将路由配置抽离出来。
新建
src/router/router.config.js
,专门用于存放路由相关配置信息。组件封装-AuthorizedRoute
接下来基于
Authorized
组件对Route
组件进行二次封装。新建
src/components/Authorized/AuthorizedRoute.jsx
。实现如下:
优化后
现在重写我们的 Router 组件。
心情舒畅了许多。
可是还留着一个问题呢——由于用户权限信息是异步获取的,在权限信息数据返回之前,
AuthorizedRoute
组件就将用户推到了redirectPath
。异步获取权限
解决思路很简单:保证相关权限组件挂载时,redux 中已经存在用户权限信息。换句话说,接口数据返回后,再进行相关渲染。
我们可以在 Layout 中进行用户信息的获取,数据获取完毕后渲染
children
。