为什么要做权限控制?

当前前端代码中控制组件显示有几种方式:

1. 通过 visible_permissions 接口返回的权限列表

  • /profile 接口返回了当前登录用户的基础信息以及一个长长的权限列表
前端权限列表
  • 有了这个列表以后,就可以在代码里这样去控制组件显示:
1
2
3
4
5
6
const tabs = [
// ... some tabs
]
if (visible_permissions.includes('tab_entity_lead')) {
tabs.push(<SomeComponent />)
}

开发上线的时候一时爽,但是未来要修改,查阅权限,就会很痛苦。
产品想不起来某个角色是否有某个权限,就来问开发,开发发现这部分代码不是自己写的,就开始全局搜,凭感觉找。找到后产品说我们要修改一下…

2. 通过 role_id 判断权限

  • 这个就更过分了,/profile 接口会返回一个 roles 字段,是一个用逗号分隔的角色组合: roles: "1,3,20"
  • 同理在代码里就是这样控制:
1
2
3
4
5
6
const tabs = [
// ... some tabs
]
if (roles.split(',').includes('1')) {
tabs.push(<SomeComponent />)
}

这么做就连权限规则的名字也没有,所以更加难整理修改。

原有的两种判断方式都是在代码中写定判断逻辑

  1. 判断方式不统一且太分散
  2. 修改某角色的权限组合要改代码逻辑,成本比较大
  3. 不容易定位到某角色某权限的当前表现

前端权限设计逻辑

分散在项目各处的权限判断必然是错误的方向,想统一去管理页面渲染,最能想到的就是用一个JSON格式的配置告诉页面这个登录用户的页面将会渲染成什么样子。

所以方案就是:

  • 为每个角色转一份JSON,并提供一个UI界面去管理控制、修改模板。
  • 每个登录用户的权限是他/她拥有的各个角色的合集。
  • React:接口给我什么,我就渲染什么。

JSON数据的层级即为前端页面组件的层级,通过遍历数据来渲染应该显示的组件。

JSON的组成为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pages: [
{
"component_type": "menu",
"contents": [
{
"component_type": "menu-page",
"contents": [
// 为空或其他同样格式的对象数据
],
"id": 4,
"key": "dashboard",
"meta": {
"icon": "menu_dashboard",
"route": "/dashboard"
},
"name": "首页"
},
]
},
{
// ...
},
// ...
]

每个对象都拥有:

  • component_type React 用于判断渲染的组件类型(Menu / Page / Section …)
  • contents 子集,所包含的层级关系
  • id id
  • key React 用于判断渲染具体组件的 key
  • meta 其他信息
  • name 中文名称

页面上应该渲染的元素全部由这个数据决定。

拆页面:页面组件和层级定义

  1. Menu (component_type: menu)
    1. 侧边栏的大类(主控台、常用功能、管理功能)
  2. Page(component_type: menu-page/single-page)
    1. 路由,不同的页面,/dashboard、/workbench、… 等
    2. 侧边栏的配置项
    3. 没有权限的路由会返回404界面
    4. 分为 menu-page(标准页) 和 single-page(详情页)
  3. Section(component_type:section-group / section)
    1. 各页面下的标签栏(支持嵌套)
    2. 必须是 section-group 作为父级,section 作为子级,section-group 下只能是 section,section 下可以继续加入 section-group 来实现嵌套
  4. 首页组件 (component_type: dashboard-item)
    1. 首页上按权限显示的组件
  5. Common
    1. 任何上面没有提到的其他需要权限的内容

页面层级示意图:
页面层级示意图

接口设计

  1. 页面组件可选配置的模板的CRUD GET POST

页面组件的模板编辑:
页面组件的模板编辑

  1. 各个角色所配的内容的CRUD GET POST

角色所拥有的的权限编辑:
角色所拥有的的权限编辑

  1. 用户登录后获取的所拥有角色合集的权限配置 GET

/fe_permissions 返回包含当前登录用户的所有页面内容(JSON)
/fe_permissions 返回的 JSON 数据

代码实现

提供了一系列内容的 Providers,JSON 配置里给出组件类型,就按需渲染对应的内容。

Components Providers
├── ContentProvider.jsx
├── DashboardProvider.jsx
├── PageProvider.jsx
├── SectionsProvider.jsx

└── index.less

举例:ContentProvider.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ContentProvider = (props) => {
const {
content,
sectionsConfig = {},
} = props

switch (content.componentType) {
case 'section-group':
return (
<SectionsProvider {...sectionsConfig} sections={content.contents} />
)
case 'dashboard-item':
return (
<DashboardProvider {...props} />
)
default:
return <div>没有该内容的渲染方式 {content.componentType}</div>
}
}

export default ContentProvider

SectionProvider.jsx

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
const SectionsProvider = (props) => {
const {
sections,
tabPaneContent,
tabsConfig = {},
customTabName = () => {},
} = props

return (
<Tabs {...tabsConfig} type='card'>
{sections.map(({ meta: { scope }, key, name, contents }) => (
<TabPane
key={scope || key}
tab={customTabName({ name, key, scope }) || name}
>
{/* 自定的内容 */}
{tabPaneContent && tabPaneContent({ scope: scope || key, key, name })}
{/* 接口里返回的内容 */}
{contents.map(content => (
<ContentProvider
key={content.key}
content={content}
sectionsConfig={{
...props,
tabsConfig: tabsConfig.subConfig,
}}
/>
))}
</TabPane>
))}
</Tabs>
)
}

在遍历整个JSON配置时渲染对应的组件。