所谓的权限控制是什么?
一般后台管理系统的权限涉及到两种:
资源权限一般指菜单、页面、按钮等的可见权限。
数据权限一般指对于不同用户,同一页面上看到的数据不同。
本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:
在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户guest没有路由/setting的访问权限,但是他知道/setting的完整路径,直接通过输入路径的方式访问,此时仍然是可以访问的。这显然是不合理的。这部分其实就属于路由层面的权限控制。
权限控制核心
需要控制前端页面显示权限,后台Api的访问、操作权限。
- 通过auth server控制scope
- Frontend通过scope确实页面显示
- Backend api统一需要通过token校验,该判断由Nginx进行过滤
- Backend api通过method和url比对,确认是否有操作权限
Login
通过登录接口,判断用户的角色,响应对应的权限给到前端
https://localhost/api/token
参数
| 12
 3
 
 | grant_type: passwordusername: admin
 password: admin
 
 | 
响应
| 12
 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,获取用户登录角色,响应对应的权限
| 12
 
 | authorization_db = AuthorizationDB()scopes = authorization_db.read(auth_type)
 
 | 
| 12
 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个角色
| 12
 3
 4
 5
 
 | class Authorization(str, Enum):root = 'root'
 super_admin = 'super_admin'
 user_admin = 'user_admin'
 viewer = 'viewer'
 
 | 
Action 操作对接的具体权限。例如:不能操作,只读权限read,能修改update
| 12
 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所有对应的页面,或者可以理解为所有的动作
| 12
 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入口页面,如果未登录显示登录页面,否则显示默认页面
| 12
 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主容器页面
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | <MenuisExtend={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获取的权限,控制用户的路由权限
| 12
 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获取的权限,控制用户的菜单展示
| 12
 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
| 12
 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
| 12
 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
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | <TwoSideMultiSelectBoxitem={{ 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错误
| 12
 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进行转换比对,判断用户是否有该接口的权限。
| 12
 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'}],
 
 |