import isEmpty from 'lodash/isEmpty'; import isArray from 'lodash/isArray'; import isString from 'lodash/isString'; import isFunction from 'lodash/isFunction'; import indexOf from 'lodash/indexOf'; const checkPermissions = (authorities, permissions) => { if (isEmpty(permissions)) { return true; } if (isArray(authorities)) { for (let i = 0; i < authorities.length; i += 1) { if (indexOf(permissions, authorities[i]) !== -1) { return true; } } return false; } if (isString(authorities)) { return indexOf(permissions, authorities) !== -1; } if (isFunction(authorities)) { return authorities(permissions); } throw new Error('[react-acl-router]: Unsupport type of authorities.'); }; export default checkPermissions;
在上面我们提到了路由的配置文件,这里我们为每一个需要鉴权的路由再添加一个属性 permissions ,即哪些角色可以访问该页面。
const routes = [{ path: '/outlets', exact: true, permissions: ['admin', 'user'], component: Outlets, unauthorized: Unauthorized, pageTitle: 'Outlet Management', breadcrumb: ['/outlets'], }, { path: '/outlets/:id', exact: true, permissions: ['admin'], component: OutletDetail, redirect: 'https://www.jb51.net/', pageTitle: 'Outlet Detail', breadcrumb: ['/outlets', '/outlets/:id'], }];
在上面的配置中,admin 和 user 都可以访问门店列表页面,但只有 admin 才可以访问门店详情页面。
对于没有权限查看当前页面的情况,一般来讲有两种处理方式,一是直接重定向到另一个页面(如首页),二是渲染一个无权限页面,提示用户因为没有当前页面的权限所以无法查看。二者是排他的,即每个页面只需要使用其中一种即可,于是我们在路由配置中可以根据需要去配置 redirect 或 unauthorized 属性,分别对应 无权限重定向 及 无权限显示无权限页面 两种处理方式。具体代码大家可以参考示例项目 react-acl-router 中的实现,这里摘录一小段核心部分。
renderRedirectRoute = route => ( <Route key={route.path} {...omitRouteRenderProperties(route)} render={() => <Redirect to={route.redirect} />} /> ); renderAuthorizedRoute = (route) => { const { authorizedLayout: AuthorizedLayout } = this.props; const { authorities } = this.state; const { permissions, path, component: RouteComponent, unauthorized: Unauthorized, } = route; const hasPermission = checkPermissions(authorities, permissions); if (!hasPermission && route.unauthorized) { return ( <Route key={path} {...omitRouteRenderProperties(route)} render={props => ( <AuthorizedLayout {...props}> <Unauthorized {...props} /> </AuthorizedLayout> )} /> ); } if (!hasPermission && route.redirect) { return this.renderRedirectRoute(route); } return ( <Route key={path} {...omitRouteRenderProperties(route)} render={props => ( <AuthorizedLayout {...props}> <RouteComponent {...props} /> </AuthorizedLayout> )} /> ); }
于是,在最终的路由中,我们会优先匹配无需鉴权的页面路径,保证所有用户在访问无需鉴权的页面时,第一时间就可以看到页面。然后再去匹配需要鉴权的页面路径,最终如果所有的路径都匹配不到的话,再渲染 404 页面告知用户当前页面路径不存在。
需要鉴权的路由和不需要鉴权的路由作为两种不同的页面,一般而言它们的页面布局也是不同的。如登录页面使用的就是普通页面布局:
在这里我们可以将不同的页面布局与鉴权逻辑相结合以达到只需要在路由配置中配置相应的属性,新增加的页面就可以同时获得鉴权逻辑和基础布局的效果。这将极大地提升开发者们的工作效率,尤其是对于项目组的新成员来说纯配置的上手方式是最友好的。
5、应用集成
至此一个包含基础权限管理的应用路由就大功告成了,我们可以将它抽象为一个独立的路由组件,使用时只需要配置需要鉴权的路由和不需要鉴权的路由两部分即可。
const authorizedRoutes = [{ path: '/outlets', exact: true, permissions: ['admin', 'user'], component: Outlets, unauthorized: Unauthorized, pageTitle: 'pageTitle_outlets', breadcrumb: ['/outlets'], }, { path: '/outlets/:id', exact: true, permissions: ['admin', 'user'], component: OutletDetail, unauthorized: Unauthorized, pageTitle: 'pageTitle_outletDetail', breadcrumb: ['/outlets', '/outlets/:id'], }, { path: '/exception/403', exact: true, permissions: ['god'], component: WorkInProgress, unauthorized: Unauthorized, }]; const normalRoutes = [{ path: 'https://www.jb51.net/', exact: true, redirect: '/outlets', }, { path: '/login', exact: true, component: Login, }]; const Router = props => ( <ConnectedRouter history={props.history}> <MultiIntlProvider defaultLocale={locale} messageMap={messages} > // the router component <AclRouter authorities={props.user.authorities} authorizedRoutes={authorizedRoutes} authorizedLayout={BasicLayout} normalRoutes={normalRoutes} normalLayout={NormalLayout} notFound={NotFound} /> </MultiIntlProvider> </ConnectedRouter> ); const mapStateToProps = state => ({ user: state.app.user, }); Router.propTypes = propTypes; export default connect(mapStateToProps)(Router);