根据用户角色隐藏一些React组件子元素

13

我正在使用React和Redux(带有Node.js后端)编写单页面应用。

我想要实现基于角色的访问控制,并且希望控制应用程序中某些部分(或子部分)的显示。

我将从Node.js获取权限列表,它只是一个具有以下结构的对象:

{
  users: 'read',
  models: 'write',
  ...
  dictionaries: 'none',
}

key 是受保护的资源,

value 是用户对此资源的权限之一(nonereadwrite)。

我将其存储到 redux 状态中。看起来很容易。

none 权限将由 react-router 路由的 onEnter/onChange 钩子或 redux-auth-wrapper 进行检查。这也很容易。

但是,如何将 read/write 权限应用于任何组件视图(例如,在 Models 组件中隐藏编辑按钮,如果用户具有 { models: 'read' } 权限)?

我找到了这个解决方案并进行了一些修改以适应我的任务:

class Check extends React.Component {
  static propTypes = {
    resource: React.PropTypes.string.isRequired,
    permission: React.PropTypes.oneOf(['read', 'write']),
    userPermissions: React.PropTypes.object,
  };

  // Checks that user permission for resource is the same or greater than required
  allowed() {
    const permissions = ['read', 'write'];
    const { permission, userPermissions } = this.props;
    const userPermission = userPermissions[resource] || 'none';

    return permissions.indexOf(userPermission) >= permissions.indexOf(permission)
  }

  render() {
    if (this.allowed()) return { this.props.children };
  }
}

export default connect(userPermissionsSelector)(Check)

其中 userPermissionsSelector 将会是以下内容:(store) => store.userPermisisons,返回用户权限对象。

然后用 Check 包装受保护的元素:

<Check resource="models" permission="write">
  <Button>Edit model</Button>
</Check>

因此,如果用户没有对modelswrite权限,则该按钮将不会显示。

有人做过类似的事情吗?是否有比这更“优雅”的解决方案?

谢谢!

P.S. 当然,服务器端也会检查用户权限。


我之前做过类似的事情。我有一个基于用户限制的管理员页面。你现在做的基本上就是我当时做的事情。我用了 PHP 作为后端,并通过检查 PHP 会话进行了验证(虽然不是最好的方法,但对我来说可行)。而且它成功地隐藏了该页面。如果你的解决方案有效,那么我不会太过担心,尽管你可能需要在渲染中添加一个else语句,以返回一些提示,例如“您没有访问此页面的权限”之类的内容,这样用户就可以看到一些东西,而不是认为页面已经崩溃了。 - A. L
谢谢,但在这种情况下不需要else语句,因为我想隐藏一些操作控件,如“编辑”或“删除”按钮,在已经打开的页面中。 - Anton Novik
如果你像这样编程 { exists && <div> some stuff </div> },并且exists是falsey,那么这个div将不存在(不仅仅是 display: none)。 - A. L
嘿...你找到这个解决方案的在线资源了吗? - llioor
嗨,不,我没有。 - Anton Novik
显示剩余3条评论
2个回答

14

我想我理解了你的要求。我已经做了一些对我有效并且我喜欢的方式,但我知道还有其他可行的解决方案。

我写的是一个类似于 HOC react-router 样式的东西。

基本上,我有一个 PermissionsProvider,在其中初始化用户权限。我有另一个 withPermissions 的 HOC,将我先前提供的权限注入到我的组件中。

因此,如果我需要在特定组件中检查权限,则可以轻松访问它们。

// PermissionsProvider.js
import React, { Component } from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";

class PermissionsProvider extends React.Component {
  static propTypes = {
    permissions: PropTypes.array.isRequired,
  };

  static contextTypes = {
    permissions: PropTypes.array,
  };

  static childContextTypes = {
    permissions: PropTypes.array.isRequired,
  };

  getChildContext() {
    // maybe you want to transform the permissions somehow
    // maybe run them through some helpers. situational stuff
    // otherwise just return the object with the props.permissions
    // const permissions = doSomething(this.props.permissions);
    // maybe add some validation methods
    return { permissions: this.props.permissions };
  }

  render() {
    return React.Children.only(this.props.children);
  }
}

const withPermissions = Component => {
  const C = (props, context) => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <Component permissions={context.permissions} {...remainingProps} ref={wrappedComponentRef} />
    );
  };

  C.displayName = `withPermissions(${Component.displayName || Component.name})`;
  C.WrappedComponent = Component;
  C.propTypes = {
    wrappedComponentRef: PropTypes.func
  };

  C.contextTypes = {
    permissions: PropTypes.array.isRequired
  };

  return hoistStatics(C, Component);
};

export { PermissionsProvider as default, withPermissions };

好的,我知道这是很多代码。但这些是高阶组件(可以在这里了解更多)。

高阶组件(HOC)是React中重用组件逻辑的高级技术。HOC并不是React API的一部分,它们是从React的组合性质中出现的模式。具体来说,高阶组件是一个函数,它接受一个组件并返回一个新的组件。

基本上我之所以这样做是因为我受到了React Router的启发。每当你想了解一些路由相关的内容时,你只需添加装饰器@withRouter,它们就会将props注入到你的组件中。那么为什么不做同样的事情呢?

  1. 首先,你必须使用提供者设置权限
  2. 然后,你在检查权限的组件上使用withPermissions装饰器

//App render
return (
   <PermissionsProvider permissions={permissions}>
      <SomeStuff />
   </PermissionsProvider>
);

SomeStuff 中,你可能有一个广泛分布的工具栏来检查权限吗?

@withPermissions
export default class Toolbar extends React.Component {
  render() {
    const { permissions } = this.props;

    return permissions.canDoStuff ? <RenderStuff /> : <HeCantDoStuff />; 
  }
}

如果您无法使用装饰器,可以像这样导出工具栏

export default withPermissions(Toolbar);

这里有一个codesandbox示例,我在其中展示了它的实际应用:

https://codesandbox.io/s/lxor8v3pkz

注意事项:

  • 我非常简化了权限,因为那个逻辑来自你这边,而且为了演示目的,我进行了简化。
  • 我假设权限是一个数组,所以在高阶组件中检查了PropTypes.array。
  • 这是一个非常长且复杂的答案,我尽力表达。请不要因为一些错误在这里和那里而对我进行质询 :)

@AntonNovik,你对此情况有何看法? - João Cunha

7

由@lokuzt建议的方法很棒。

为了简化您的代码,您可以采取更进一步的措施。

首先,每个受保护的组件都有一些需要满足以进行呈现的要求。您需要定义一个函数,将要求和当前用户的凭据作为参数传递以进行呈现。它必须返回truefalse

function isSatisfied(requirement, credentials) {
  if (...) {
    return false;
  }

  return true;
}

进一步地,我们需要使用ReactJS的新上下文API来定义一个HOC(高阶组件)
const { Provider, Consumer } = React.createContext();

function protect(requirement, WrappedComponent) {
  return class extends Component {
    render() {
      return (
        <Consumer>
          { credentials => isSatisfied(requirement, credentials)
            ? <WrappedComponent {...this.props}>
                {this.props.children}
              </WrappedComponent>
            : null
          }
        </Consumer>
      );
    }
  }; 
}

现在您可以装饰您的组件:
const requireAdmin = {...}; // <- this is your requirement

class AdminPanel extends Component {
  ...
}

export default protect(requireAdmin, AdminPanel);

甚至第三方组件:
import {Button} from 'react-bootstrap';

const AdminButton = protect(requireAdmin, Button);

需要通过ReactJS上下文API传递凭据:

class MyApp extends Component {
  render() {
    const {credentials} = this.props;

    <Provider value={credentials}>
      ...

         <AdminPanel/>

         <AdminButton>
           Drop Database
         </AdminButton>
      ...
    </Provider>
  }
}

这是我在github上的扩展实现

演示也同样可用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接