所谓的权限控制是什么?

一般后台管理系统的权限涉及到两种:

  • 资源权限
  • 数据权限

资源权限一般指菜单、页面、按钮等的可见权限。

数据权限一般指对于不同用户,同一页面上看到的数据不同。

本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:

  • 侧边栏菜单
  • 路由权限

在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户guest没有路由/setting的访问权限,但是他知道/setting的完整路径,直接通过输入路径的方式访问,此时仍然是可以访问的。这显然是不合理的。这部分其实就属于路由层面的权限控制。

权限控制核心

需要控制前端页面显示权限,后台Api的访问、操作权限。

  1. 通过auth server控制scope
  2. Frontend通过scope确实页面显示
  3. Backend api统一需要通过token校验,该判断由Nginx进行过滤
  4. Backend api通过method和url比对,确认是否有操作权限

Login

通过登录接口,判断用户的角色,响应对应的权限给到前端

https://localhost/api/token

参数

1
2
3
grant_type: password
username: admin
password: admin

响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiX2FkbWluIiwidXNlcm5hbWUiOiJhZG1pbiIsImF1dGhvcml6YXRpb24iOiJzdXBlcl9hZG1pbiIsImFkZGl0aW9uYWxfZnVuY3Rpb25zIjpbImFjdGl2aXR5X2xvZ3MiXSwiaWF0IjoxNjg3MjQyOTk2fQ.wQWt85Xpmx6BV4WAs65k8Kb57NsTYzNgoBUXkmUQiRQ",
"token_type": "bearer",
"scope": [
"action:devices",
"read:devices",
"update:devices",
"update:device_logs",
"update:groups",
"update:guests",
"update:identities",
"update:recognition_callback",
"update:recognition_update_guest_model",
"update:recognition_update_server_model",
"update:server",
"update:users"
]
}

通过该API,获取前端页面的Action权限。

Auth Server

校验服务auth_service.py

读取DB,获取用户登录角色,响应对应的权限

1
2
authorization_db = AuthorizationDB()
scopes = authorization_db.read(auth_type)
1
2
3
4
5
6
7
def read(self, auth_type):
session = get_db_session()
session_query = session.query(self._item).filter(self._item.auth_type == auth_type)

result = [self._formatter(row) for row in session_query.all()]

return result

DB

用户的角色Role,区分为4个角色

1
2
3
4
5
class Authorization(str, Enum):
root = 'root'
super_admin = 'super_admin'
user_admin = 'user_admin'
viewer = 'viewer'

Action 操作对接的具体权限。例如:不能操作,只读权限read,能修改update

1
2
3
4
5
6
7
8
9
10
11
class ScopeAction(str, Enum):
read = 'read'
create = 'create'
update = 'update'
delete = 'delete'
add = 'add'
remove = 'remove'
action = 'action'
develop = 'develop'
export = 'export'
import_ = 'import'

Scope所有对应的页面,或者可以理解为所有的动作

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
class ScopeObject(str, Enum):
devices = 'devices'
identities = 'identities'
faces = 'faces'
fingerprints = 'fingerprints'
guests = 'guests'
groups = 'groups'
events = 'events'
cards = 'cards'
device_logs = 'device_logs'
users = 'users'
server = 'server'
organizations = 'organizations'
groups_to_devices = 'groups_to_devices'
groups_from_devices = 'groups_from_devices'
groups_to_identities = 'groups_to_identities'
groups_from_identities = 'groups_from_identities'
faces_from_identities = 'faces_from_identities'
cards_from_identities = 'cards_from_identities'
faces_to_guests = 'faces_to_guests'
faces_from_guests = 'faces_from_guests'
devices_to_groups = 'devices_to_groups'
devices_from_groups = 'devices_from_groups'
identities_to_groups = 'identities_to_groups'
identities_from_groups = 'identities_from_groups'
organizations_to_users = 'organizations_to_users'
organizations_from_users = 'organizations_from_users'
users_to_organizations = 'users_to_organizations'
users_from_organizations = 'users_from_organizations'
guest_faces = 'guest_faces'
face_features = 'face_features'
features = 'features'
recognition_callback = 'recognition_callback'
recognition_update_guest_model = 'recognition_update_guest_model'
recognition_update_server_model = 'recognition_update_server_model'
files = 'files'
upgrade_schedules = 'upgrade_schedules'
download_log = 'download_log'
debug = 'debug'
face_quality = 'face_quality'
credentials = 'credentials'

Frontend

前端如何利用权限,控制页面的显示,按钮的展示。

App.jsx入口页面,如果未登录显示登录页面,否则显示默认页面

1
2
3
4
5
6
7
8
9
10
11
12
13
<div>
{"token" in sessionStorage ? (
R.equals(props.location.pathname, "/") ? (
<Redirect to="/devices" />
) : (
dispatch({ type: "SET_LIST_DETAIL" }) && (
<Container {...props} />
)
)
) : (
<Redirect to="/login" />
)}
</div>

Container.jsx主容器页面

1
2
3
4
5
6
7
8
9
10
11
12
<Menu
isExtend={isExtend}
setIsExtend={setIsExtend}
{...props}
ref={{ menuRef: menuRef, contentContainer: contentContainer }}
/>
<div className={styles.contentContainer} ref={contentContainer}>
<img src={devideLine} className={styles.devideLine} />
<ContentTop isExtend={isExtend} setIsExtend={setIsExtend} {...props} />
<Content devicesList={devicesList} {...props} />
<BatchOperate {...props} />
</div>

component/container/Content.jsx

Router,控制路由展示,通过Api获取的权限,控制用户的路由权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Switch>
{scope.find((e) => R.equals("read:devices", e)) && (
<Route path="/devices" render={(props) => <Devices {...props} />} />
)}
{scope.find((e) => R.equals("read:acs", e)) && (
<Route path="/acs" render={(props) => <ACs {...props} />} />
)}
{scope.find((e) => R.equals("read:groups", e)) && (
<Route
path="/groups"
render={(props) => <Groups devicesList={devicesList} {...props} />}
/>
)}
</Switch>

component/container/Menu.jsx

菜单控制,通过Api获取的权限,控制用户的菜单展示

1
2
3
4
5
6
7
8
{scope.find((e) => R.equals("read:devices", e)) && (
<NavLink to="/devices">
</NavLink>
)}
{scope.find((e) => R.equals("read:groups", e)) && (
<NavLink to="/groups">
</NavLink>
)}

具体页面,是否显示某个按钮page/devices/Devices.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
scope.find((e) => R.equals("create:devices", e)) &&
buttonOptions.push({
type: "ADD",
subOptions: [
{
name: intl.formatMessage({
id: "ADD_NEW",
defaultMessage: "add new",
}),
onClick: () => props.history.push("/devices/new"),
},
],
});

通过组件,控制页面展示page/devices/DetailPage.jsx

1
2
3
4
5
6
<GridDetails>
<SettingBlocks setIsDisabled={setIsDisabled} {...props} />
{scope.find((e) => R.equals("develop:debug", e)) && (
<DevelopmentMode debug={debug} setDebug={setDebug} />
)}
</GridDetails>

page/devices/SettingBlocks.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
<TwoSideMultiSelectBox
item={{ group_ids: data.data }}
setItem={(data) =>
dispatch({
type: "UPDATE_GROUP_IDS",
data: data.group_ids,
})
}
isEditable={Boolean(
scope.find((e) => R.equals("update:devices", e))
)}
{...props}
/>

Backend

Nginx

通过Nginx控制,所有的API请求,必须通过权限验证

nginx.conf 请求后台服务器,判断权限

否则,会提示403错误

1
2
3
4
5
6
7
8
9

location /api/ {
auth_request /auth;
auth_request_set $auth_user_id $upstream_http_user_id;
auth_request_set $auth_user_username $upstream_http_user_username;
auth_request_set $auth_user_authorization $upstream_http_user_authorization;
auth_request_set $auth_user_info_name $upstream_http_user_info_name;
set $forward_api http://api-v2-server:8081$request_uri;
}

Backend Server

后台服务,需要中间件,获取用户的Url和Method

然后将Method 和url进行转换比对,判断用户是否有该接口的权限。

1
2
3
4
5
6
7
f'{ScopeAction.action}:{ScopeObject.devices}':                  [{'method': 'POST',   'url': '/devices/*/actions'},
{'method': 'PUT', 'url': '/devices/*/admin_lock'},
{'method': 'PUT', 'url': '/devices/*/register'},
{'method': 'POST', 'url': '/devices/*/register'},
{'method': 'GET', 'url': '/devices/*/register/*'},
{'method': 'DELETE', 'url': '/devices/*/register/*'}],
f'{ScopeAction.export}:{ScopeObject.devices}': [{'method': 'GET', 'url': '/devices/export'}],