权限系统设计相关概念与思路

前言: 权限系统的设计几乎是每个系统都必需的模块,目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。从万物皆文件的Linux文件权限到各种CRM、CMS、OA管理系统,都可以看到权限系统的身影。

权限管控分类

如果从控制力的角度来进行划分的话,权限管控可以分为功能级权限管控(功能权限)和数据级权限管控(数据权限)。

  • 功能权限

指用户登录系统后能看到哪些模块,操作哪些按钮。常见的后台管理系统,不同业务的人员,登录系统后,看到的功能模块也不尽相同;而同属于一个部门,因为职位等级不同,拥有的操作功能也可能不同(例如组长、负责人才拥有删除权限)。

  • 数据权限

指用户在某个模块里能看到哪些范围的数据,如A部门的销售人员只能看到自己的客户数据,但是A部门的销售总监可以查看整个区域销售人员的客户数据。

认证与授权

权限系统进行权限控制,主要包含了两部分,认证与授权。

认证(Authentication)

身份验证(Authentication)又称“认证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。身份验证的目的是确认当前所声称为某种身份的用户,确实是所声称的用户。一般使用的技术手段有:HTTP Basic Authentication、HMAC(AK/SK)、OAuth2、JWT等,可以参考文章:https://chinalhr.github.io/post/api-auth-program

授权(Authorization)

授权Authorization)一般是指对信息安全或计算机安全相关的资源定义与授予访问权限,尤指访问控制。在权限系统中体现为授予认证用户的功能权限与数据权限。

功能权限模型

主流的功能权限模型包括ACL(Access Control List)访问控制列表、RBAC(Role-based access control)基于角色的访问控制模型、ABAC(Attribute-based access control)基于属性的访问控制。

ACL

访问控制表(Access Control List,ACL),又称存取控制串列,是使用以访问控制矩阵为基础的访问控制表,每一个(文件系统内的)对象对应一个串列主体。访问控制表由访问控制条目(access control entries,ACE)组成。访问控制表描述用户或系统进程对每个对象的访问控制权限。

ACL是一种面向资源的访问控制模型,每一项资源,都配有一个列表,这个列表记录的就是哪些用户可以对这项资源执行CRUD中的那些操作。当系统试图访问这项资源时,会首先检查这个列表中是否有关于当前用户的访问权限,从而确定当前用户可否执行相应的操作。

ACL核心在于用户可以直接和权限挂钩,设计简单但缺点也是很明显的,由于需要维护大量的访问权限列表,所以在性能上有明显的不足,因而便有了对ACL设计的改进如RBAC、ABAC。

访问控制矩阵如下所示:

资产 1 资产 2 文件 设备
角色 1 read, write, execute, own execute read write
角色 2 read read, write, execute, own

图片

RBAC

RBAC 以角色为基础的访问控制,认为权限授权的过程可以抽象地概括为:Who是否可以对What进行How的访问操作,并对这个逻辑表达式进行判断是否为True的求解过程,也即是将权限问题转换为What、How的问题,Who、What、How构成了访问权限三元组。

包含了三个基础组成部分,分别是:用户角色权限

RBAC通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离(区别于ACL模型),方便权限的管理。

image

RBAC0-RBAC核心思想模型

RBAC0是RBAC模型的核心,主要包含以下

  • User:用户,每个用户都有唯一的UID识别,并被授予对应的角色
  • Role:角色,不同角色具有不同的权限
  • Permission:权限,包含对应访问功能的权限
  • Session:会话,存储特定用户会话的基本权限信息

图片

RBAC1-基于角色的分层模型

RBAC1是对RBAC进行了扩展,对RBAC的角色进行了分层处理,引入了角色的继承概念,有了继承的关系就有了上下级的包含关系。

图片

RBAC2-RBAC约束模型

RBAC2是对RBAC进行了扩展,主要引入了SSD(静态职责分离)和DSD(动态职责分离)。

SSD主要应用在用户和角色之间(授权阶段),主要约束

  1. 互斥角色,同一个用户不能授于互斥关系的角色
  2. 基数约束,一个用户拥有的角色是有限的,一个角色拥有的许可是有限的
  3. 先决条件约束,用户想得到高级权限,必须先拥有低级权限

DSD是会话和角色之间的约束,主要动态决定怎么样计划角色,如:一个用户拥有5个角色,只能激活2个。

图片

RBAC3

RBAC3就是整合了RBAC1+RBAC2模式,这里不再阐述。

图片

ABAC

基于属性的访问控制(ABAC),也称为基于策略的访问控制(IAM),定义了访问控制范式,其中通过使用将属性组合在一起的策略,将访问权限授予用户。这些策略可以使用任何类型的属性(用户属性,资源属性,对象,环境属性等)。该模型支持布尔逻辑,其中规则包含谁在发出请求,资源和操作的“ IF,THEN”语句。例如:如果请求者是管理者,则允许对敏感数据的读/写访问。

对比RABC模型,ABAC的主要区别在于表示复杂布尔规则集的策略的概念可以评估许多不同的属性,更加灵活可控。

基于属性的访问控制(ABAC)维度

  • 外部授权管理
  • 动态授权管理
  • 基于策略的访问控制
  • 细粒度的授权

ABAC组件

Attribute:属性,用于表示 subject、object 或者 environment conditions 的特点,attribute 使用 key-value 的形式来存储这些信息。

Subject:指代使用系统的人或者其他使用者(non-person entity,NPE),比如说客户端程序,访问 API 的 client 或者移动设备等等。一个 subject 可以有多个的 attributes,例如用户属性等。

Object:指代 ACM(访问控制机制) 需要管理的资源,如文件,某项记录,某台机器或者某个网站, object也可以有多项属性,比如x组的测试、生产实例。

Operation:subject需要做的操作,比如查看某条记录,使用某个功能,登录某台服务器。往往包括我们常说的读、写、修改、拷贝等等,一般 operation 是会表达在 request 中的,比如 HTTP method。

Policy:通过 subject、object 的 attribute 与 environment conditions 一起来判断 subject 的请求是否能够允许的关系表示,一般是一系列的 boolean 逻辑判断的组合。

Environment Conditions:表示目前进行的访问请求发生时的操作或情境的上下文。Environment conditions 常常用来描述环境特征,是独立于 subject 与 object 的,常用来描述系统的情况:比如时间,当前的安全等级,生产环境还是测试环境等等。

图片

一个典型的 ABAC 场景描述如下图,当 subject 需要去读取某一条记录时,我们的访问控制机制在请求发起后遍开始运作,该机制需要计算,来自 policy 中记录的规则,subject 的 attribute,object 的 attribute 以及 environment conditions,而最后会产生一个是否允许读取的结果,也就是上图的allow or deny。

阿里云的RAM访问控制运用的就是ABAC模型:

 {
          "Version": "1",
          "Statement":
            [{
              "Effect": "Allow",
              "Action": ["oss:List*", "oss:Get*"],
              "Resource": ["acs:oss:*:*:samplebucket", "acs:oss:*:*:samplebucket/*"],
              "Condition":
                 {
                    "IpAddress":
                     {
                        "acs:SourceIp": "42.160.1.0"
                      }
                  }
             }]
    }

组织架构与功能权限

常见的组织架构如下所示是一个树形的结构,最顶部节点为根部门,根部门对应多个子部门,子部门也可以对应多个子部门。

图片

每个组织部门下都会有多个岗位,比如研发部有后端开发、前端开发、技术经理、技术总监…职位,虽然都在同一部门,但是每个职位的权限是不同的,职位高的拥有更多的权限。

图片

组织架构结合功能权限:可以把岗位与角色进行关联,用户加入某个岗位后,就会自动获得该岗位的全部角色,减少授权操作,同时用户在调岗时,角色即可批量调整。

基于组织架构模型与RBAC0的模型图如下所示:

图片

数据权限

数据权限解决的是用户能看到多少数据量和什么数据的问题,例如A和B两个用户都能看到订单模块,但A能看到320条数据,B只能看到100条数据,且A能看到的320条数据中包含着B能看到的100条数据,这些都是由数据权限决定的。

数据权限一般和组织架构相关,常见的基于组织结构的数据权限类型有如下几种:

  • 只能查看本人数据
  • 只能查看本部门数据
  • 只能查看本部门及其子部门数据
  • 只能查看自定义部门数据
  • 只能查看自定义部门及其子部门数据
  • 可以查看所有部门数据

组织架构一般为树状的架构,而常见的数据权限一般和部门产生过滤关系,且不同部门的不同岗位有着不同的数据权限,如销售部门的销售组长岗位可以看到本部门的所有数据,而销售部门的销售员岗位只能看到自己本人的数据;因此可以基于上文对组织架构与功能权限的模型,添加岗位和数据权限的关联关系,如下图所示:

image

因为数据权限涉及到每张表里面的每一行数据,如何设计数据权限的查询模型,合理地利用数据库索引的特性,提高查询的效率显得至关重要;

按照正常的思路,数据入库的时候我们会存储对应的用户ID(creator_id)与对应部门的ID(department_id)。数据权限的获取就包括了获取用户绑定的数据权限类型(枚举值)、用户可以查看哪些部门的数据(遍历部门、子部门)或者本人的数据(user_id);权限数据的筛选我们可以通过筛选部门的ID与用户ID进行实现。

主要优化点有两点:

  1. 数据权限的获取(权限部门数据获取涉及多次IO操作)
  2. 权限数据的筛选(避免通过in查询筛选数据导致大数据量下存在性能问题,合理利用索引特性)

一种基于组织结构的数据权限设计

图片

为每个部门指定一个data_key,并通过一定规则进行data_key的配置,使其与自己的上级部门、下级部门产生关联,如上图所示:根部门的data_key为0,直属部门的data_key为0 001、0 002…以此类推,根据此模型,结合常见relational database 如mysql的前置查询索引特性,可以通过获取部门的data_key即可查询本部门数据或者本部门及其子部门数据,并且可以合理的利用索引的特性,提高性能。

示例:

-- 1. 用户在所在部门data_key 0001001,数据权限:本部门
select * from bill where data_key = '0001001';
-- 2. 用户在所在部门data_key 0001,数据权限:本部门及其子部门
select * from bill where data_key like '0001%';
-- 3. 用户id 372,数据权限:本人
select * from bill where creator_id = 372;

功能权限与数据权限系统设计实现实践

依据上述对功能权限,数据权限模型的描述,我们可以简单设计一个拥有组织架构、基于RBAC-0的功能权限,基于组织架构的数据权限系统。系统使用的技术栈为 Kotlin、Spring Boot…

表结构设计

图片

功能权限系统设计

  • 认证

这里我们可以基于JWT机制实现认证,用户登录后颁发JWT Token充当用户的登录凭证,JWT Token Payload存储了用户的ID;当JWT Token过期时让用户重新登录。这里可以基于Refresh Token、Redis实现Token的刷新机制,具体细节不在本文进行讨论。

  • 授权

以一个Spring Boot项目为例子,我们可以通过实现HandlerInterceptor拦截所有的api接口,在preHandle的地方解析token获取用户的id,再查询出用户所拥有的的所有接口权限、数据权限、origanization_key…,授权于本次请求(session)通过ThreadLocal存放于请求的上下文中。

/**
 * 授权上下文对象
 * */
class GrantPermissionContext(
        var interfacePermissionKey: List<String>,
        var dataPermissionType: DataPermissionType,
        var adminUserId: Long,
        var organizationKey: String,
        var enabledStatus: EnabledStatus

) {

  companion object {
    private val GRANT_PERMISSION_CONTEXT = ThreadLocal<GrantPermissionContextV2>()

    fun get(): GrantPermissionContext? {
      return GRANT_PERMISSION_CONTEXT.get()
    }

    fun set(interfacePermissionKey: List<String>,
            dataPermissionType: DataPermissionType,
            adminUserId: Long,
            enabledStatus: EnabledStatus,
            organizationKey: String
    ) {
      val grantPermissionContext = GrantPermissionContextV2(
              interfacePermissionKey = interfacePermissionKey,
              dataPermissionType = dataPermissionType,
              adminUserId = adminUserId,
              enabledStatus = enabledStatus,
              organizationKey = organizationKey
      )
      GRANT_PERMISSION_CONTEXT.set(grantPermissionContext)
    }

    fun clean() {
      GRANT_PERMISSION_CONTEXT.remove()
    }
  }
}
/**
 * 授权拦截器
 * */
@Component
class GrantPermissionInterceptor : HandlerInterceptor {

  override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {

   	//...解析JWT Token,获取授权信息设置到GrantPermissionContext中
    GrantPermissionContext.set(
            interfacePermissionKey = interfacePermissionKey,
            dataPermissionType = dataPermissionType,
            adminUserId = userId,
            enabledStatus = enabledStatus
            organizationKey = organizationKey
    )
    return true
  }

  override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) {
    //清除GrantPermissionContext
    GrantPermissionContext.clean()
  }

}
  • 鉴权

可以通过annotation + Aspect,对设置了对应注解的接口进行拦截鉴权

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RequiresPermissions(val permissionKey: String)
@Aspect
@Component
@Order(value = 1)
class InterfacePermissionAspect {

  @Around("@annotation(requiresPermissions)")
  fun doInterfacePermissionBefore(joinPoint: ProceedingJoinPoint, requiresPermissions: RequiresPermissions): Any? {

    val grantPermissionContext = GrantPermissionContext.get()
    val interfacePermissionKey = grantPermissionContext?.interfacePermissionKey
            ?: throw NotInterfacePermissionException()
    val permissionKey = requiresPermissions.permissionKey
    if (!interfacePermissionKey.contains(permissionKey) || grantPermissionContext.enabledStatus == EnabledStatus.DISABLE) {
      throw NotInterfacePermissionException()
    }
    return joinPoint.proceed()
  }
}

数据权限设计

  • 数据筛选

可以利用MyBatis的Interceptor机制、或者 Spring Data JPA 的Filter机制,在SQL执行之前通过GrantPermissionContext获取对应的数据权限类型与organizationKey,对SQL进行统一的数据权限过滤筛选条件拼接。