所谓的权限控制是什么?
一般后台管理系统的权限涉及到两种:
资源权限一般指菜单、页面、按钮等的可见权限。
数据权限一般指对于不同用户,同一页面上看到的数据不同。
本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:
在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户guest
没有路由/setting
的访问权限,但是他知道/setting
的完整路径,直接通过输入路径的方式访问,此时仍然是可以访问的。这显然是不合理的。这部分其实就属于路由层面的权限控制。
权限控制核心
需要控制前端页面显示权限,后台Api的访问、操作权限。
- 通过auth server控制scope
- Frontend通过scope确实页面显示
- Backend api统一需要通过token校验,该判断由Nginx进行过滤
- 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'}],
|