Простой дизайн разрешений на основе React

React.js
Простой дизайн разрешений на основе React

Управление разрешениями на внешнем интерфейсе предназначено только для взаимодействия с пользователем, соответствующая роль отображает соответствующее представление, а реальная безопасность находится на заднем плане.

предисловие

В начале выпуска основное содержание работы заключалось в разработке системы фонового управления Явлением, существовавшим в то время, было:

Если пользователь помнит определенный URL-адрес и напрямую вводит его в браузере, он или она может войти на страницу независимо от того, есть ли у пользователя разрешение на доступ к странице.

Если страница инициализируется (componentDidMount) для выполнения запроса интерфейса серверная часть вернет код состояния HTTP 403, а интерфейсная часть инкапсулируетrequest.jsИсключения, не связанные с бизнесом, будут обработаны, и если будет обнаружена ошибка 403, она будет перенаправлена ​​на неавторизованную страницу.

Если при инициализации страницы нет внешнего и внутреннего взаимодействия, описанный выше процесс не будет запущен до тех пор, пока пользователь не инициирует определенные действия (например, отправку формы).

Видно, что гарантия безопасности — это задняя часть, так что же может сделать передняя часть?

  1. Четко информируйте пользователей о том, что у них нет разрешений, чтобы пользователи ошибочно не думали, что у них есть разрешение на работу (даже если операция не удалась), и сразу переходили на страницу без разрешений;
  2. Перехватывать четкие и неавторизованные запросы, такие как записи определенных операций (кнопки или навигация и т. д.), которые требуют разрешений и не отображаются неавторизованным пользователям.Фактически этот пункт включает в себя предыдущий пункт.

Я смотрел его недавноAnt Design ProНеобходимо сделать сводку обработки, связанной с разрешениями.

Следует отметить, что, хотя данная статья основана наAnt Design ProИдея дизайна разрешения не является полной интерпретацией его исходного кода (может быть более склонна к идее v1, без участия umi).

Если есть ошибки и недоразумения, пожалуйста, шлепните и исправьте их, спасибо.

Обработка разрешений на уровне модуля

Предположим, что существует следующая связь:

роль роль Полномочия перечисления значение авторитета логика
обычный пользователь user Не показывай
администратор admin Отображение кнопки «Войти в админку Backstage»

На определенной странице есть кнопка с текстом "Войти в фон управления", которая видна только администратору, давайте реализуем.

Простая реализация

// currentAuthority 为当前用户权限枚举值

const AdminBtn = ({ currentAuthority }) => {
  if ('admin' === currentAuthority) {
    return <button>进入管理后台</button>;
  }
  return null;
};

Ну, это супер просто.

контроль доступа естьif else, реализация функции не сложна, главное, что обработка каждой страницы|модуля|кнопки должна оцениваться один раз, и требования всегда могут быть реализованы.

Однако теперь это просто кнопка на странице, и мы столкнемся со многими сценариями, когда «определенный xxx существует на определенной странице (несколько) и отображается только для xxx (или/и xxx)».

Значит, может лучше.

Ниже показан один из самых основных компонентов управления разрешениями.Authorized.

Инкапсуляция компонентов — разрешено

Ожидаемая форма вызова выглядит следующим образом:

<Authorized currentAuthority={currentAuthority} authority={'admin'} noMatch={null}>
  <button>进入管理后台</button>
</Authorized>

Апи выглядит следующим образом:

параметр иллюстрировать Типы По умолчанию
children Элементы, которые отображаются нормально, отображаются при вынесении решения о разрешении ReactNode
currentAuthority текущие разрешения string
authority права доступа string/string[]
noMatch Отображается, когда решение о разрешении не принято ReactNode

currentAuthorityНет необходимости передавать это свойство вручную каждый раз при его вызове, здесь предполагается, что информация о пользователе передается черезreduxПолучить и хранить по всему мируstoreсередина.

Примечание. Конечно, мы можем повесить информацию о пользователе вwindowвниз илиlocalStorageОднако очень важно, что в большинстве сценариев мы получаем данные асинхронно через интерфейс, что очень важно. еслиhtmlразмещенный на бэкэнде илиssrВ случае с сервером информация о пользователе вводится непосредственно на сервер, что действительно хорошо.

новый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Когда это строка (текущая роль имеет только одно разрешение).

Обработка разрешений на уровне страницы

страница размещенаRouteМодули в разделе Компоненты.

Зная это, мы можем легко написать следующий код:

новый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;

Этот код не работает, так как текущая информация о правах доступа получается асинхронно через интерфейс, в это времяAuthorizedКомпонент не может получить текущее разрешение (currentAuthority), при доступе напрямую через URL/user-pageили/admin-page, независимо от того, совпадает ли личность пользователя, результат запроса не будет возвращен, он будет перенаправлен на/loginили/403, этот вопрос будет обсуждаться позже.

Давайте сначала оптимизируем наш код.

Извлечь конфигурацию маршрутизации

Слишком много jsx-контента, связанного с конфигурацией маршрутизации, слишком много страниц сложно поддерживать, читабельность сильно снижена Мы можем извлечь конфигурацию маршрутизации.

новый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',
  },
];

Инкапсуляция компонентов — AuthorizedRoute

Далее на основеAuthorizedпара компонентовRouteКомпоненты переупакованы.

новый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.

фактическиAnt Design Proверсии v4 имеют эту проблему, по сравнению с v2@/pages/Authorizedкомпоненты изlocalStorageИнформация о разрешениях получается из v4, а данные вместо этого получаются из редукса (данные в редуксе получаются через интерфейс), что аналогично этой статье. конкретный видимыйЭтот пиар.

Получить разрешения асинхронно

Решение очень простое: убедитесь, что информация о правах пользователя уже существует в избыточности, когда соответствующие компоненты разрешений смонтированы. Другими словами, после возврата данных интерфейса выполняется соответствующий рендеринг.

Мы можем получить информацию о пользователе в Layout и отрендерить после получения данныхchildren.

Эпилог

Ant Design ProНачиная с v2 нижний слой основан наumiРеализация, настроенная маршрутизациейRoutesсвойства, комбинированные@/pages/Authorizedкомпонент (основанный на@/utils/AuthorizedКомпоненты -@/components/Authorizedвторичная инкапсуляция, инъекцияcurrentAuthority(текущие разрешения)) для реализации основного процесса. В то же время информация о разрешениях хранится вlocalStorage,пройти через@/utils/authority.jsПредоставленные инструменты и методы для разрешенияgetтак же какset.

Смотри внимательно@/components/AuthorizedСодержимое файла также содержалоAuthorizedRouteкомпонент, но в коде не используется (вместо@/pages/Authorizedкомпонент), я просмотрел вопрос и узнал, что v1 не основан наumi, на основеAuthorizedRouteДля управления полномочиями маршрутизации после обновленияAuthorizedRouteОн не используется для управления разрешениями маршрутизации.

Задействовано много связанных файлов (components/pages/utils), а документации по версии 4 немного не хватает.Если вы посмотрите на исходный код, если вы не разберетесь с различиями между версиями, это будет очень трудоемко.

В этой статье информация о разрешениях получается асинхронно через интерфейс и сохраняется в редуксе (аналогично версии v4, см.@/pages/Authorizedтак же как@/layouts/SecurityLayout).