权限管理

对于一个 Web 应用,权限管理是经常会涉及的一个话题,在介绍权限管理方案之前,我们先来梳理下常见的权限管理包含哪些方面:

  • 路由鉴权:当用户访问某个没有权限的页面时跳转到无权限页面
  • 接口鉴权:当用户通过操作调用没有权限的接口时跳转到无权限页面
  • 操作鉴权:页面中的某些按钮或区块针对无权限的用户直接隐藏
  • 菜单鉴权:某些菜单针对无权限的用户直接隐藏(属于操作鉴权的一种)

权限组件

设计一个权限组件首先要知道当前用户的角色,角色信息通常存在服务端,因此我们需要服务端将当前用户的角色输出到前端,推荐服务端将当前用户角色写在 cookie 中,然后在前端代码中读取角色信息。

接着我们来实现权限组件:

// src/components/Auth/index.jsx
import React from 'react';
import cookie from 'cookie';

// Exception 组件需要业务自己实现
import Exception from '@/components/Exception';

/**
 * Auth Component
 *
 * <Auth authorities={['admin']} hidden={true}>
 *  <Button />
 * </Auth>
 *
 * @params {array} authorities 允许哪些角色使用
 * @params {boolean} hidden 无权限时是否直接隐藏
 */
export function Auth({ children, authorities = [], hidden }) {
  // 服务端将当前用户角色 authority 保存在 cookie 中,前端负责读取 cookie
  const { authority } = cookie.parse(document.cookie);
  const hasAuth = authorities.indexOf(authority) !== -1;

  if (hasAuth) {
    // 有权限直接渲染内容
    return children;
  } else {
    // 无权限
    if (hidden) {
      // 无权限则不显示内容
      return null
    } else {
      // 无权限则显示指定 UI,也可以跳转到统一的无权限页面
      return (
        <Exception
          statusCode="403"
          description="抱歉,你没有权限访问该页面"
        />
      );
    }
  }
};

/**
 * HOC 方式使用
 *
 * withAuth({
 *  authorities: ['admin'],
 * })(ListPage);
 */
export function withAuth(params) => (WrapperedComponent) => {
  return (props) => {
    return (
      <Auth {...params}>
        <WrapperedComponent {...props} />
      </Auth>
    );
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

使用 Auth 组件

路由鉴权

路由鉴权针对页面级组件,直接在组件层面使用上面的 withAuth 控制页面的权限:

// src/pages/List
import React from 'react';
import { withAuth } from '@/components/Auth';

function List() {
  return (
    <div className="list-page">
      <Table />
    </div>
  );
}

// 仅 admin 角色可访问该页面
export default withAuth({
  authorities: ['admin'],
})(List);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

接口鉴权

请参考文档前后端通信,业务上封装统一的请求方法,与服务端约定接口协议,前端根据状态码判断无权限、未登录等状态,然后跳转到指定页面。

操作鉴权

如果页面中某个组件要根据角色判断是否显示,可以使用 Auth 组件,同时传入 hidden={true}

<Auth authorities={['admin']} hidden={true}>
  <Button>删除</Button>
</Auth>
1
2
3

菜单鉴权

当需要对某些菜单项进行权限控制,只需在对应的菜单项设置 authorities 属性即可,当前用户的权限如果与 authorities 中指定的权限不匹配,菜单项将不显示。

// src/config/menu.js
const headerMenuConfig = [
  // ...
  {
    name: '反馈',
    path: 'https://github.com/alibaba/ice',
    external: true,
    newWindow: true,
    icon: 'message',
    authorities: ['admin'], // 当前用户权限为 admin 时显示菜单,否则隐藏
  },
];

const asideMenuConfig = [
  {
    name: '文章管理',
    path: '/post',
    icon: 'edit',
    authorities: ['admin', 'user'], // 当前用户权限为 admin 或者 user 时显示菜单,否则隐藏
    children: [
      {
        name: '文章列表',
        path: '/post/list',
        authorities: ['user'], // 当前用户权限为 user 时显示菜单,否则隐藏
      },
      {
        name: '添加文章',
        path: '/post/create'
      },
    ],
  }
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

控制菜单项渲染也是使用 Auth 组件实现,当菜单项有配置 authorites 属性时,使用 Auth 组件包裹,权限不匹配时组件隐藏。

/**
 * 根据权限决定是否渲染某个表单项
 * @param {object} item - 菜单项组件
 * @param {array} authorities - 菜单项允许权限数组
 * @param {string} key - 当前菜单项的 key
 * @return {object} 渲染的菜单项
 */
function renderAuthItem(item, authorities, key) {
  if (authorities) {
    return (
      <Auth
        authorities={authorities}
        hidden
        key={key}
      >
        {item}
      </Auth>
    );
  } else {
    return item;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22