- PVSM.RU - https://www.pvsm.ru -

Подход к реализации RBAC в ReactJS

Вступление

Привет, дорогой читатель!

Некоторое время (около года) назад столкнулся с необходимостью условной отрисовки компонентов в ReactJS в зависимости от текущих прав пользователя. Первым делом начал искать готовые решения и «лучшие практики». Статья "Role based authorization in React [1]" произвела больше всего впечатления своим использованием Higher-Order Components [2] (HOC). Но, к сожалению, решения, которое меня удовлетворяет, не нашел.

Видимо, все-таки что-то упустил...

… или не знал о существовании контекстов. На момент написания статьи наткнулся на замечательный ответ в stackoverflow [3]. У меня в итоге получилось сильно похожее решение.

В то время был немного знаком с «react-redux-connect» (npm-модуль), и меня сильно зацепил подход с декорированием, который используется в функции connect. Подробный разбор устройства connect можно найти тут [4].

Описание решения

Для начала надо определить какая минимальная информация нужна для принятия решения об отрисовке компонента. Очевидно, для отрисовки необходимо выполнение некоторого условия (как вариант наличие какого-то права — например, право добавления нового пользователя). Назовем это условие требованием (или requirement на английском). Понять, было ли требование удовлетворено, можем на основе набора текущих прав пользователя — credentials. То есть достаточно определить функцию:

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

Теперь мы более или менее определились с условием отрисовки. А как это использовать?

1. Можем использовать подход в лоб:

const requirement = {...};

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

    return isSatisfied(requirement, credentials) && <TargetComponent>;
  }
}

2. Можем пойти чуть дальше, и обернуть целевой компонент в другой, который и будет делать проверку выполнения требования:

const requirement = {...};

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

    return (
      isSatisfied(requirement, credentials)
        ? <TargetComponent {...this.props}>
            {this.props.children}
          </TargetComponent>
        : null
    );
  }
}

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

    return <ProtectedTargetComponent/>;
  }
}

Вручную писать обертку для каждого целевого компонента довольно муторно. Как это можем упростить?

3. Можем прибегнуть к механизму HOC (по аналогии с connect из «react-redux-connect»):

function protect(requirement, WrappedComponent) {
  return class extends Component {
    render() {
      const { credentials } = this.props;

      return (
        isSatisfied(requirement, credentials)
          ? <WrappedComponent {...this.props}>
              {this.props.children}
            </WrappedComponent>
          : null
      );
    }
  }
}
...
const requireAdmin = {...};
const AdminButton = protect(requireAdmin, Button);
...
class App extends Component {
  render() {
    const {credentials} = this.props;

   return (
     ...
       <AdminButton credentials={credentials}>
         Add user
       </AdminButton>
     ...
   );
  }
}

Уже лучше, но всё еще убого — нужно руками пробрасывать credentials через всё дерево компонентов. Что с этим можно сделать? Логично предположить, что credentials текущего пользователя — это глобальный объект для всего приложения. Тогда на помощь снова приходит «react-redux-connect». Почитав статью об устройстве этого модуля, обнаруживаем, что в нём используются некие контексты [5] ReactJS.

4. С использованием механизма контекстов получаем окончательный подход:

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 = {...};
const AdminButton = protect(requireAdmin, Button);
...
class App extends Component {
  render() {
    const { credentials } = this.props;
    return (
        <Provider value={credentials}>
        ...
           <AdminButton>
             Add user
           </AdminButton>
        ...
        </Provider>
    );
  }
}

Послесловие

Это был краткий экскурс в саму идею. На базе этой идеи был реализован модуль (github [6], npm [7]), который предоставляет более интересные возможности и его проще встроить (см. README.md в гитхабе и демо [8] с использованием модуля).

Только мне почему-то не удалось завести созданный npm пакет в демо, поэтому пришлось туда вставлять сам код модуля. Но модуль, установленный через npm install react-rbac-guard, локально работает (Chrome 69.0.3497.100). Подозреваю, что проблема в способе сборки — я просто скопировал файлы package.json и webpack.config.prod.js из модуля [9] с аналогичным предназначением.

Так как я не являюсь фронтенд разработчиком, ещё много чего недоделанного (отсутствие тестов, неработоспособность в https://codesandbox.io [10] и, возможно, другие упущенные моменты). Поэтому, если будут замечания, предложения или пулл-реквесты, то добро пожаловать!

P.P.S.: Все замечания по поводу правописания, в том числе в README.md, просьба присылать в личные сообщения или в виде пулл-реквеста.

Автор: Nurzhan69

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/297425

Ссылки в тексте:

[1] Role based authorization in React: https://hackernoon.com/role-based-authorization-in-react-c70bb7641db4

[2] Higher-Order Components: https://reactjs.org/docs/higher-order-components.html

[3] ответ в stackoverflow: https://stackoverflow.com/questions/42567575/hide-some-react-component-children-depending-on-user-role/47579290#47579290

[4] тут: https://medium.com/devschacht/jakob-lind-code-your-own-redux-part-2-the-connect-function-d941dc247c58

[5] контексты: https://reactjs.org/docs/context.html

[6] github: https://github.com/nurzhan-saktaganov/react-rbac-guard

[7] npm: https://www.npmjs.com/package/react-rbac-guard

[8] демо: https://codesandbox.io/s/znmxlw59jm

[9] модуля: https://github.com/brainhubeu/react-permissible

[10] https://codesandbox.io: https://codesandbox.io

[11] Источник: https://habr.com/post/428143/?utm_campaign=428143