Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于 React 的简单权限设计 #8

Open
worldzhao opened this issue May 9, 2021 · 2 comments
Open

基于 React 的简单权限设计 #8

worldzhao opened this issue May 9, 2021 · 2 comments
Labels

Comments

@worldzhao
Copy link
Owner

前端进行权限控制只是为了用户体验,对应的角色渲染对应的视图,真正的安全保障在后端。

前言

毕业之初,工作的主要内容便是开发一个后台管理系统,当时存在的一个现象是:

用户若记住了某个 url,直接浏览器输入,不论该用户是否拥有访问该页面的权限,均能进入页面。

若页面初始化时(componentDidMount)进行接口请求,后端会返回 403 的 HTTP 状态码,同时前端封装的request.js会对非业务异常进行相关处理,遇见 403,就重定向到无权限页面。

若是页面初始化时不存在前后端交互,那就要等用户触发某些操作(比如表单提交)后才会触发上述流程。

可以看到,安全保障是后端兜底的,那前端能做些什么呢?

  1. 明确告知用户没有权限,避免用户误以为自己拥有该权限而进行操作(即使无法操作成功),直接跳转至无权限页面;
  2. 拦截明确无权的请求,比如某些需要权限才能进行的操作入口(按钮 or 导航等)不对无权用户展示,其实本点包含上一点。

最近也在看Ant Design Pro的权限相关处理,有必要进行一次总结。

需要注意的是,本文虽然基于Ant Design Pro的权限设计思路,但并不是完全对其源码的解读(可能更偏向于 v1 的涉及思路,不涉及 umi)。

如果有错误以及理解偏差请轻捶并指正,谢谢。

模块级别的权限处理

假设存在以下关系:

角色 role 权限枚举值 authority 逻辑
普通用户 user 不展示
管理员 admin 展示“进入管理后台”按钮

某页面上存在一个文案为“进入管理后台”的按钮,只对管理员展示,让我们实现一下。

简单实现

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

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

好吧,简单至极。

权限控制就是if else,实现功能并不复杂,大不了每个页面|模块|按钮涉及到的处理都写一遍判断就是了,总能实现需求的。

不过,现在只是一个页面中的一个按钮而已,我们还会碰到许多“某(几)个页面存在某个 xxx,只对 xxx(或/以及 xxx) 展示”的场景。

所以,还能做的更好一些。

下面来封装一个最基本的权限管理组件Authorized

组件封装-Authorized

期望调用形式如下:

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

api 如下:

参数 说明 类型 默认值
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;

这段代码是不 work 的,因为当前权限信息是通过接口异步获取的,此时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 改为从 redux 中获取(redux 中的数据则是通过接口获取),和本文比较类似。具体可见此次 PR

异步获取权限

解决思路很简单:保证相关权限组件挂载时,redux 中已经存在用户权限信息。换句话说,接口数据返回后,再进行相关渲染。

我们可以在 Layout 中进行用户信息的获取,数据获取完毕后渲染children

@lh2218431632
Copy link

如果有许多组件都需要相同的功能的话,就可以使用HOC模式

@worldzhao
Copy link
Owner Author

如果有许多组件都需要相同的功能的话,就可以使用HOC模式

没错,只用 HOC 可以更方便的定制相关的 fallback 逻辑,而配置化路由扩展起来就会耦合性过高,可以按需选择

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants