点击关注“有赞coder”
获取更多技术干货哦~
部门:零售移动
权限管理是一个几乎所有大中型 B 端系统都会涉及的重要组成部分,其目的是对整个系统进行权限控制,避免造成误操作及数据泄露等风险问题。在充分调研了商家的经营需求后,传统的老板、店长、收银员等角色不足以覆盖商家角色场景。因此,在原有权限系统的基础上,增加了商家自主定义员工权限的能力,满足其细粒度管控员工权限的诉求。任何一家使用有赞开店软件经营店铺的商家,不仅能给员工赋予默认的角色,也可以实时给员工开放特定的权限、修改权限,以此保障店铺运营的安全、健康。
在讲述有赞使用的权限模型之前,先介绍一下权限相关的基本概念:
权限:用户可操作行为的最小单位。
用户:每个用户都有唯一标识,并被授予一个或多个角色。
角色:由不同的权限组合而成,最终分配给具体用户。
权限管理:控制用户的权限,只能访问授权内容。
ACL(Access Control List):基于用户级别的权限控制。
将系统的各种权限直接授予具体的用户。抽象来说,为每个用户维护了单独的权限列表,当需要分配权限、收回权限时,需要修改对应用户的权限信息。
RBAC(Role Base Access Control):基于角色级别的权限控制。
与 ACL 对比,RBAC不用给用户单个分配权限,权限与用户之前通过角色关联。通过给不同的角色分配不同的权限,只需要将用户指向对应的角色就会有对应的权限。分配权限、收回权限只需要通过修改用户的角色即可。
ABAC(Attribute Base Access Control):基于属性级别的权限控制。
不同于常见的将用户通过某种方式直接关联到权限的方式,ABAC 是通过动态计算一个或一组属性来是否满足某种条件来进行权限判断。
属性一般分为四类:用户属性(自然人属性,如年龄、性别等),环境属性(物理环境,如时间、地点、气候),操作属性(读、写)和对象属性(操作对象,如资金、某张图片、某个特定的页面,又称资源属性)。
因此理论上能够实现灵活的权限控制、将在权限与用户之前通过一组或多组属性实现关联,几乎能满足所有类型的需求。
基于线下经营的物理场景,有赞需要研发一套更灵活的权限管理系统,能将商家的权限需求,具象为多个不同的、支持商家自由勾选、定制的角色。从灵活性层面看,不需要对每个员工逐一做个性化定制,只需要对某一类员工做权限个性化定制(包含默认权限授权)。在权衡了权限的灵活性需求、管理维护难度、性能瓶颈之后,有赞最终选择RBAC 模型研发权限管理系统。
抽象来看权限体系可以分为如下两类:功能权限与数据权限两部分。
功能权限指的是在系统中的功能可否使用,通常我们将功能权限分为查看、编辑、删除等,同时编辑、删除权限又包含了查看。通过小的权限点拆分更精细的赋予了员工能否进入某个页面查看信息、编辑信息的能力。
数据权限指数据中存在的数据是否能查看,是一个更细粒度的权限。比如一个页面,不同角色查看不同的数据就需要通过数据权限控制。
从管理对象维度又可以分为:店铺能力 与 员工能力。
店铺能力店铺维度的权限,比如有赞的商业化插件,可以通过店铺能力去体现。
员工能力赋予员工的权限,比如收银开单、资金管理等。
店铺能力优先级绝对高于员工能力,所有场景的权限判断,店铺能力必须先于员工能力。简单地说,店铺能力决定了“店铺能做什么”,员工能力决定了“用户能做什么”。
SAM平台是之前在使用的权限系统,使用的权限模型是 RBAC 模型。它的核心思路是将所有的权限结果抽象成一个 64 位的 long 型。使用方在查询某个权限点时,需将权限点与后端返回的权限集做一个位运算。
后端返回的权限数据如图所示:
//权限点
{
"menuId": 113101101,
"menuName": "网店查看",
"mapBizPerms": {
"retail": [0, 0, 1125899906842624]
}
}
//员工个人能力
{
"retail": [4611686018427387903, -144115188075855873, -3]
}
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, 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)
}
}
}