有赞移动权限体系建设
The following article is from 有赞coder Author 有赞技术
一、背景
权限管理是一个几乎所有大中型 B 端系统都会涉及的重要组成部分,其目的是对整个系统进行权限控制,避免造成误操作及数据泄露等风险问题。在充分调研了商家的经营需求后,传统的老板、店长、收银员等角色不足以覆盖商家角色场景。因此,在原有权限系统的基础上,增加了商家自主定义员工权限的能力,满足其细粒度管控员工权限的诉求。任何一家使用有赞开店软件经营店铺的商家,不仅能给员工赋予默认的角色,也可以实时给员工开放特定的权限、修改权限,以此保障店铺运营的安全、健康。
二、权限与权限管理
2.1 名词定义
在讲述有赞使用的权限模型之前,先介绍一下权限相关的基本概念:
权限:用户可操作行为的最小单位。
用户:每个用户都有唯一标识,并被授予一个或多个角色。
角色:由不同的权限组合而成,最终分配给具体用户。
权限管理:控制用户的权限,只能访问授权内容。
2.2 模型选择
ACL(Access Control List):基于用户级别的权限控制。
将系统的各种权限直接授予具体的用户。抽象来说,为每个用户维护了单独的权
限列表,当需要分配权限、收回权限时,需要修改对应用户的权限信息。
RBAC(Role Base Access Control):基于角色级别的权限控制。
与 ACL 对比,RBAC不用给用户单个分配权限,权限与用户之前通过角色关联。
通过给不同的角色分配不同的权限,只需要将用户指向对应的角色就会有对应的
权限。分配权限、收回权限只需要通过修改用户的角色即可。
ABAC(Attribute Base Access Control):基于属性级别的权限控制。
不同于常见的将用户通过某种方式直接关联到权限的方式,ABAC 是通过动态计
算一个或一组属性来是否满足某种条件来进行权限判断。
属性一般分为四类:用户属性(自然人属性,如年龄、性别等),环境属性(物理环境,如时间、地点、气候),操作属性(读、写)和对象属性(操作对象,如资金、某张图片、某个特定的页面,又称资源属性)。
因此理论上能够实现灵活的权限控制、将在权限与用户之前通过一组或多组属性实现关联,几乎能满足所有类型的需求。
基于线下经营的物理场景,有赞需要研发一套更灵活的权限管理系统,能将商家的权限需求,具象为多个不同的、支持商家自由勾选、定制的角色。从灵活性层面看,不需要对每个员工逐一做个性化定制,只需要对某一类员工做权限个性化定制(包含默认权限授权)。在权衡了权限的灵活性需求、管理维护难度、性能瓶颈之后,有赞最终选择RBAC 模型研发权限管理系统。
2.3 权限管控
抽象来看权限体系可以分为如下两类:功能权限 与 数据权限 两部分。
功能权限指的是在系统中的功能可否使用,通常我们将功能权限分为查看、编辑、
删除等,同时编辑、删除权限又包含了查看。通过小的权限点拆分更精细的赋予
了员工能否进入某个页面查看信息、编辑信息的能力。
数据权限指数据中存在的数据是否能查看,是一个更细粒度的权限。比如一个
页面,不同角色查看不同的数据就需要通过数据权限控制。
从管理对象维度又可以分为:店铺能力 与 员工能力。
店铺能力店铺维度的权限,比如有赞的商业化插件,可以通过店铺能力去体现。
员工能力赋予员工的权限,比如收银开单、资金管理等。
店铺能力优先级绝对高于员工能力,所有场景的权限判断,店铺能力必须先于员
工能力。简单地说,店铺能力决定了“店铺能做什么”,员工能力决定了“用户能
做什么”。
三. 权限系统1.0(SAM)
SAM平台是之前在使用的权限系统,使用的权限模型是 RBAC 模型。它的核心思路是将所有的权限结果抽象成一个 64 位的 long 型。使用方在查询某个权限点时,需将权限点与后端返回的权限集做一个位运算。
后端返回的权限数据如图所示:
//权限点
{
"menuId": 113101101,
"menuName": "网店查看",
"mapBizPerms": {
"retail": [0, 0, 1125899906842624]
}
}
//员工个人能力
{
"retail": [4611686018427387903, -144115188075855873, -3]
}
配置难: 权限需求实现频繁依赖后端资源。在日常开发中,如果要新增权限,首 先需要业务方提供一个权限需求,而权限需求有由权限描述、角色类型、店铺类 型组成,后端资源到位后,实现该需求、发布至相应的测试环境,同时将权限信 息同步至端侧同学,端侧同学继续完成接下来的代码适配。这条链路流程多、沟 通成本高、资源依赖重、排队耗时长。 排查难:原来的设计,导致端上也存在权限计算,且计算方式依赖位运算,无法 直接定位问题所在。在排查权限问题时,业务往往需要先进行复杂的计算,当问 题定位在权限数据出错时,需要再次依赖后端资源,进行数据的修正和发布。 扩展难:权限属于中台应用,承接了微商城、零售、教育等业务方,终端类型有 pc、移动端 APP、小程序、deskTop 等。SAM 系统提供的能力比较单一,无法 为各端提供可定制能力。原有的菜单功能是参考 pc 设计的,无法满足移动端页 面级别的的多类型组件需求。
四.机遇与挑战
业务语言“说人话”:权限应当隔离业务语言与实现语言,对接方不再需要理解晦 涩的权限业务语言,“权限由业务定义”。 配置方式由原来的“工具”向“操作平台”改变:权限由原来的“开发工具”平台化, 并向运营平台转变,将单侧的资源压力平衡到需求方。 端侧升级架构,提供更丰富的能力:提供权限解析能力,解放业务在权限判断中 的生产力。
五.权限系统2.0
rig 系统(Rig:北欧神话中彩虹桥的守护神):添加了权限表达层,以相对友好 的方式呈现给业务方,替换以往将实现逻辑直接暴露给业务方的方式。 权限配置平台:权限配置平台支持业务域配置不同店铺类型的权限,支持各端根 据店铺类型、平台配置不同的菜单。需求方不再依赖后端资源,开发、产品或运 营可直接根据权限需求、基于店铺类型、角色、不同的平台配置权限、菜单。 dynamicMenu:移动端组件动态化方案。需求方在配置平台增删、编辑APP的 工作台菜单后,菜单内所有的组件经过权限校验后下发,且兼容零售移动的特殊 业务模式(如离线模式)。
权限后台
菜单服务:获取渲染菜单。 API 服务:API 校验。 角色服务:获取角色、同步角色、增删角色等。 用户服务:用户新增、更新、删除、
权限中台
移动端
工作台架构
JS Engine:JS 实现的跨端计算引擎,提供统一的组件解析、权限和店铺能力解 析 、移动端扩展能力解析。 WigetManager:负责管理移动端所有的 widget 管理、路由分发、widget 形 态预处理。 UI 组件层:提供基础的组件如表单、网格、按钮等。 UI 展示层:业务根据配置,纵向展示对应的组件。
工作台实现
@Nav("dynamicMenuWidget/UrgentNotice")
class UrgentNoticeWidgetFragment : BaseRetailWidgetFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.home_widget_urgent_notice, container, false)
}
//数据获取成功回调
override fun loadSuccess(data: UrgentNoticeResponse?) {}
//数据获取失败回调
override fun loadError(t: Throwable) {}
//widget点击事件
override fun onWidgetClicked() {}
}
val fragment = Navigator.newInstallOrNull<BaseWidgetFragment>("dynamicMenuWidget/UrgentNotice")
class WidgetDelegate{
val widgetMap:HashMap<String,BaseWidgetFragment>
val activity: Activity
var rootWidgetId: String = ""
val children = ArrayList<WidgetDelegate>()
var parentDelegate: WidgetDelegate? = null
constructor(activity: Activity, rootWidgetId: String, parentWidgetDelegate: WidgetDelegate? = null
) {}
constructor(fragment: BaseWidgetFragment) {
/**
* 加载widget
* containerId:容器id
* doAction: 业务方自定义处理数据
*/
fun setupUI(fragmentManager: FragmentManager,@IdRes containerId: Int, beforeAction: (List<WidgetInfo>.() -> List<WidgetInfo>) = { this }) {}
/**
* widget刷新,用于下拉刷新等
*/
fun reload() {}
/**
* 通过widgetId获取widget
*/
fun findWidgetById(widgetId: String): BaseWidgetFragment? {}
}
//通过menuId注册点击事件
DynamicMenuGlabalListenerManager.addOnWidgetClickedListener("widget_finc_stock_search"){context,widgetInfo ->
service.query(){response->
if(response.success){
startActivity()
}else{
showToast(response.msg)
}
}
}
将组件测试与权限解耦,测试不再需要回归权限相关的用例。 业务组件不需要鉴权,JSEngine 进行了严密的权限校验,没有数据越权风险。 除了支持权限相关的所有配置、还支持版本控制、组件的随时上下架。 移动端不同平台机型上,保证了内容的完全一致性。
六.价值回顾
七.结语
往期推荐
技术琐话
以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。本号由坐馆老司机技术团队维护。