电子书地址:https://workflow-engine-book. 权限系统,例如:ACL[1], RBAC[2], ABAC[3]等,它们本质上,都是在解决谁(Subject)可以对什么资源(Object)进行什么操作(Action)这个问题。 ACL权限模型Access Control List(ACL,访问控制列表)是一种常见的权限管理技术,它用于定义谁可以访问特定资源以及可以执行的操作。ACL通常用于文件系统、数据库、网络设备等环境中,用于控制对资源的访问。 一个ACL是一个列表,其中每一项都包含一个主体(如用户或用户组)和一组权限。主体可以是单个用户、用户组或者所有用户(通常表示为“所有人”或“公共”)。权限则定义了主体可以对资源执行的操作,如读取、写入、执行等。 例如在API设计时应用ACL模型,权限可以这样设计: 用户(Subject) | 操作(Action) | 资源(Object) | Alice | GET | /article | Alice | PUT | /article | Bob | GET | /article | Bob | DELETE | /article |
上面的ACL策略表示Alice可以去访问和修改文章,Bob可以访问和删除文章,其他人则没有任何权限。 同样,在Linux系统中,文件的访问权限基于ACL(Access Control List,访问控制列表)设计。 Linux文件系统中的每个文件或目录都有一个关联的ACL,用于定义三类主体对文件的访问权限:文件所有者(user)、文件所属的用户组(group)以及其他用户(other)。每一类主体可以拥有读(r)、写(w)和执行(x)三种权限。 Linux文件的ACL通常表示为一个由10个字符组成的字符串,如“-rw-r--r--”。这个字符串从左到右分为四部分: 1. 第一个字符表示文件类型:'-'表示普通文件,'d'表示目录,'l'表示符号链接等。 2. 接下来的三个字符表示文件所有者的权限:'r'表示可读,'w'表示可写,'x'表示可执行。没有某个权限的话,对应的位置会被标为'-'。 3. 再接下来的三个字符表示文件所属用户组的权限,同样用'r'、'w'和'x'表示。 4. 最后三个字符表示其他用户的权限,也是用'r'、'w'和'x'表示。
例如,下面的resource_2文件ACL字符串为“-rw-r-----”,表示: 1. 这是一个普通文件(-)。 2. 文件所有者具有读(r)和写(w)权限。 3. 文件所属的用户组具有读(r)权限。 4. 其他用户没有任何权限。
[root@VM-32-12-tencentos test]# ll total 4 drwxr-xr-x 2 root root 4096 Dec 9 12:59 resource_1 -rw-r----- 1 root root 0 Dec 9 12:59 resource_2
在Linux系统中,可以使用chmod 命令修改文件的ACL。例如,要将一个文件的权限设置为所有者可读写,用户组和其他用户只可读,可以执行以下命令: chmod 644 filename
此外,Linux还支持扩展的ACL(Extended Access Control List),它允许为特定用户或用户组分配更细粒度的权限。扩展ACL可以使用getfacl 和setfacl 命令进行查询和设置。例如,要为用户Alice添加对文件的读写权限,可以执行以下命令: setfacl -m u:alice:rw resource_2
通过getfacl可以查看到设置的ACL策略: [root@VM-32-12-tencentos test]# getfacl resource_2 # file: resource_2 # owner: root # group: root user::rw- user:alice:rw- group::r-- mask::rw- other::---
从上面的举例,可以看到ACL提供了一种灵活的权限管理机制,可以支持各种复杂的访问控制需求。 然而,管理大量的ACL可能会变得复杂,特别是在大型系统中。像前面API接口设计使用ACL模型,那么意味着后续每来一个新客户都需要添加对应的策略,随着新用户的增多,这个ACL策略表也会变得越来越大很难维护。 因此,许多系统还提供了角色或属性等其他机制,以简化权限管理和提高安全性。 RBAC权限模型RBAC的基本组成尽管ACL提供了一种灵活的权限管理机制,但在实际应用中,它也存在一些问题,例如: 1. 管理复杂性:当系统中的资源和用户数量较大时,管理大量的ACL可能变得非常复杂。为每个文件或资源分配权限可能会导致管理负担加重,尤其是当需要修改或更新权限时。 2. 难以追踪和审计:由于权限是分散在各个资源的ACL中的,追踪和审计用户的访问权限可能变得困难。例如,要查找具有特定权限的所有用户,可能需要检查所有资源的ACL。 3. 权限维护困难:当用户的角色或职责发生变化时,可能需要修改多个ACL以更新用户的权限。这不仅耗时,而且容易出错。 4. 缺乏抽象和封装:ACL直接将权限分配给用户或用户组,缺乏对权限的抽象和封装。这可能导致权限管理变得繁琐和低效。
RBAC(Role-Based Access Control,基于角色的访问控制)模型通过引入角色的概念来解决这些问题,使得权限管理更加简单、高效和可维护。在RBAC模型中,权限不再直接分配给用户,而是分配给角色。用户根据需要分配一个或多个角色,从而间接获得角色所包含的权限。
以下是RBAC模型的关键组成部分: 1. 用户(Users):用户是系统中的实体,如人员、服务或应用程序。用户需要访问系统资源以完成特定任务。 2. 角色(Roles):角色是一组相关权限的集合,通常与特定职责或职位相对应。例如,管理员、普通用户和访客等。角色应该具有明确的职责和权限范围,遵循最小权限原则,即只分配角色所需的最小权限。 3. 权限(Permissions):权限是对资源的访问或操作的授权。例如,对文件的读、写和删除权限。在RBAC模型中,权限分配给角色,而不是直接分配给用户。 4. 资源(Resources):资源是系统中需要受到保护的对象,如文件、数据库表、服务等。资源可以根据需要分配给角色,以控制用户对资源的访问和操作。
RBAC的优点
通过以上步骤,可以实现基于RBAC模型的权限设计,这个模型可以弥补前面ACL模型的各种缺陷,具体来说就是有以下这些优势: 1. 简化管理:通过将权限分配给角色而非直接分配给用户,RBAC模型简化了权限管理。管理员只需要管理角色和用户与角色之间的关系,而不是为每个用户分配权限。 2. 更易于追踪和审计:在RBAC模型中,权限集中在角色中,更容易追踪和审计用户的访问权限。例如,要查找具有特定权限的所有用户,只需检查与该权限相关的角色,然后查找分配了这些角色的用户。 3. 权限维护更简单:当用户的角色或职责发生变化时,只需修改用户的角色分配,而无需逐个修改资源的ACL。这使得权限维护更加简单和高效。 4. 权限抽象和封装:RBAC模型通过角色对权限进行抽象和封装,使得权限管理更加模块化和结构化。这有助于提高权限管理的可维护性和可扩展性。
如上图所示,一个基于RBAC的权限系统可以分成如下三大块进行管理: 权限管理前面,我们说了权限管理本质上都是在解决谁(Subject)可以对什么资源(Object)进行什么操作(Action)这个问题。 资源对应的就是限制用户能访问的数据范围,操作就是限制用户能对这些资源做哪些行为。 这两个都是在后台做的一个权限限制,前端用户只能通过错误提示来得知。但在实际设计权限管理系统时,为了更好的用户体验,我们还会加多一个前端的权限控制,包括:菜单、页面、按钮的控制显示,如果没有对应权限就不显示相应的菜单、页面和按钮。没有权限的的操作,前端就直接不可见,这样用户体验上会更好。 所以,一个权限管理分别从如下方面来做限制:
前端权限在实际实现时,一般通过控制菜单的可视范围来配置,比较少具体到页面和按钮这种细粒度的控制。 例如下面某个系统,一般系统左侧是菜单栏,包含一级或二级菜单,而对应到角色权限配置页面上,则是对应把菜单的所有层级结构展示出来,然后供管理员进行配置。对应的菜单会关联API操作权限。 所以在配置每个API的时候,还需要去整理关联每个API归属于哪个菜单,可以通过#号来区分层级。一般在设计接口路径时,每个路径的中间名称可以对应一个菜单,例如下面的/v1/work路径对应的Dashboard#工作台,/v1/index路径对应的是Dashboard#首页。这样API设计和菜单关联逻辑上保持一致,更容易后期整理维护。 API权限 | 菜单 |
| /v1/user/list | 用户管理 |
| /v1/user/edit | 用户管理 |
| /v1/index/index | Dashboard#首页 |
| /v1/work/overview | Dashboard#工作台 |
|
那么角色在关联菜单的时候,实际上就关联了对应的API接口操作权限。 角色管理在RBAC(Role-Based Access Control,基于角色的访问控制)模型中,角色管理是一个关键的组成部分。角色管理主要涉及角色的创建、分配、修改和删除等操作。以下是RBAC模型中角色管理的主要步骤: 1. 角色定义:首先,需要定义系统中的角色。角色通常对应于组织中的职位或职责,如“管理员”、“编辑”、“访客”等。每个角色都应该有一个明确的职责和权限范围。 2. 权限分配:为每个角色分配一组相应的权限。权限通常是对系统资源的访问或操作的授权,如“读”、“写”、“删除”等。 3. 用户与角色关联:将用户分配到一个或多个角色。用户通过角色获得访问系统资源的权限。 4. 角色修改和删除:当组织或业务需求发生变化时,可能需要修改或删除角色。例如,当一个角色的职责发生变化时,可能需要修改该角色的权限;当一个角色不再需要时,可以删除该角色。
在实际应用中,角色管理通常需要配合用户管理和权限管理一起使用。例如,当新用户加入系统时,需要为其分配适当的角色;当系统资源或权限策略发生变化时,可能需要更新角色的权限。 在进行角色管理时,需要采取如下的安全原则进行合理设计来提高系统安全性: · 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小的权限集合 · 责任分离原则:可以通过调用相互独立互斥的角色共同完成敏感的任务,例如要求一个记账员和财务管理员共同参与统一过账操作 · 数据抽象原则:可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读、写等执行权限
用户管理用户管理主要做两方面工作流: RBAC的四个层次在RBAC模型中,为了更好地描述和理解不同程度的角色管理和访问控制,研究者将RBAC模型分为四个层次:RBAC0、RBAC1、RBAC2和RBAC3。这些层次从简单到复杂,逐步增加了角色管理和访问控制的功能。 RBAC01. RBAC0:RBAC0是RBAC模型的最基本层次,它包括用户(User)、角色(Role)和权限(Permission)三个基本元素。在RBAC0中,用户通过分配角色来获得权限。RBAC0模型关注于将权限分配给角色,以及将角色分配给用户。它不涉及角色之间的关系,也不包括角色继承。这种在大部分系统上是最常见的设计,可以满足绝大部分系统的权限模块设计需求。
RBAC1RBAC1:RBAC1在RBAC0的基础上引入了角色继承(Role Hierarchy)机制。角色继承允许一个角色继承另一个角色的权限。这种层次化的角色结构有助于简化权限管理,使权限分配更加结构化和模块化。 角色继承的主要特点如下: 1. 层次化:角色继承允许创建具有层次结构的角色。在这种结构中,一个角色可以继承一个或多个父角色的所有权限。这有助于组织和管理角色,使权限分配更加结构化和模块化。 2. 权限累积:当一个角色继承另一个角色时,它将自动获得被继承角色的所有权限。这意味着在分配权限时,只需为每个角色分配特定的权限,而无需重复分配共享的权限。 3. 灵活性:角色继承提供了一种灵活的方式来管理和分配权限。当需要调整权限时,只需修改角色继承关系,而无需逐个修改用户的权限。这使得权限管理更加灵活和高效。
举个例子,假设我们有以下角色: 在这个例子中,我们可以使用角色继承来实现以下权限分配: · 一般员工(Employee)具有基本的访问权限,如查看文件、发送电子邮件等。 · 经理(Manager)继承一般员工(Employee)的所有权限,并具有额外的权限,如审批报告、管理项目等。 · 系统管理员(System Administrator)继承经理(Manager)的所有权限,并具有额外的权限,如管理用户、配置系统等。
RBAC2RBAC2:RBAC2在RBAC1的基础上引入了约束(Constraint)机制。约束用于限制用户与角色、角色与权限之间的关联,以增强访问控制的安全性和灵活性。约束可以是静态的(例如,一个用户最多可以分配两个角色),也可以是动态的(例如,一个用户不能同时拥有某两个互斥的角色)。 约束可以分为静态约束和动态约束,以下是一些常见的RBAC2约束类型及示例: 1. 互斥角色(Mutually Exclusive Roles):互斥角色约束要求一个用户不能同时拥有某些特定的角色。这可以防止潜在的冲突或滥用权限。例如,在一个银行系统中,一个用户可能不能同时拥有“出纳员”和“审计员”的角色,以防止内部欺诈。 2. 基数约束(Cardinality Constraint):基数约束限制了分配给用户的角色数量或分配给角色的权限数量。这有助于遵循最小权限原则,降低潜在的安全风险。例如,一个用户最多可以分配两个角色;或者一个角色最多可以拥有五个权限。 3. 权责分离约束(Separation of Duties,SoD):权责分离约束要求将潜在冲突的任务分配给不同的角色。这有助于防止滥用权限和内部欺诈。例如,在一个采购系统中,“采购申请者”和“采购审批者”应该是两个独立的角色,以确保采购过程的透明和公正。 4. 前置角色约束(Prrequisite Roles Constraint):只有当用户已是角色 B 的成员时,才能将其分配给角色 A。例如要经历过主管的角色之后,才能晋级总监角色。
在实际应用中,可以根据具体的业务需求和安全策略来定义和实施RBAC2约束。这些约束可以通过编程逻辑或数据库规则等方式来实现。 RBAC3RBAC3 = RBAC1+RBAC2,既有角色继承机制也有约束机制。 总之,RBAC0、RBAC1、RBAC2和RBAC3是RBAC模型的四个层次,它们从简单到复杂,逐步增加了角色管理和访问控制的功能。在实际应用中,可以根据具体的业务需求和场景来选择合适的RBAC层次,对于大部分中小系统来说,RBAC0是够用的了。 案例改进例如前面ACL的例子改成RBAC的模型实现,可以这样设计: 首先是设计角色关联的权限: 角色(Role) | 操作(Action) | 资源(Object) | Editor | GET | /article | Editor | PUT | /article | Editor | DELETE | /article | Viewer | GET | /article |
然后是关联角色和用户: 用户(Subject) | 角色(Role) | Alice | Editor | Bob | Viewer |
这样Alice就拥有Editor这个角色的所有权限,而Editor角色可以实现对文章的读取、更改和删除,而Bob是Viewer角色,则只能查看文章。 ABAC权限模型尽管RBAC(Role-Based Access Control,基于角色的访问控制)模型相对于ACL模型简化了权限管理,提高了安全性和可维护性,但它也存在一些缺点或局限性: 1. 静态角色:RBAC模型中的角色通常是预先定义好的,这可能导致在面对复杂和动态的访问控制需求时,灵活性较低。例如,如果需要根据时间、地点或其他上下文信息来控制访问权限,RBAC模型可能难以满足需求。 2. 角色爆炸:在某些场景下,为了满足细粒度的访问控制需求,可能需要创建大量的角色,这会导致角色管理变得复杂,也称为“角色爆炸”。 3. 缺乏属性支持:RBAC模型主要依赖于角色来控制访问权限,而不支持基于用户、资源或环境等属性的访问控制。这可能导致在需要支持基于属性的访问控制的场景中,RBAC模型难以满足需求。
ABAC(Attribute-Based Access Control,基于属性的访问控制)模型正是为了解决这些问题而提出的。ABAC模型允许基于用户、资源、操作和环境等多个属性来控制访问权限。这些属性可以是静态的(例如用户的部门、资源的类型)或动态的(例如当前时间、用户的位置)。 ABAC模型通过引入属性和动态访问控制策略,解决了RBAC模型在复杂和动态访问控制场景中的缺点和局限性,使得权限管理更加灵活和高效。然而,实现ABAC模型可能相对复杂,需要对属性和策略进行管理和维护。 PERM元模型PERM(Policy, Effect, Request, Matchers)建模语言(PERM modeling language, 简称PML),是一种用于访问控制的模型。模型中的每个元素都有其特定的含义和作用: 1. Request(请求):请求是用户试图执行的操作,通常一个基础的请求是一个三元组,包括用户身份(subject)、目标资源(object)和操作类型(action)信息,即:r = {sub, obj, act} 。例如,用户试图删除一个文件,这就构成了一个请求。 2. Policy(策略):策略是定义谁可以执行哪些操作,或者在哪些条件下可以访问哪些资源的规则,即:p = {sub, obj, act}或p={sub, obj, act, eft} 。例如,策略可能会规定“管理员可以删除所有文件”,或者“只有在工作时间内,员工才能访问公司数据库”。 3. Matchers(匹配器):匹配器是用来比较请求(Request)和策略(Policy)的工具,如果请求和策略匹配,那么将执行策略定义的效果,匹配器可以是简单的比较操作,也可以是复杂的逻辑表达式。例如:m = r.sub == p.sub && r.action == p.action && r.resource == p.resource ,这个简单的匹配规则表示,如果请求的参数(subject, object, action) 能在策略中找到对应的匹配结果,则返回策略结果p.eft ,其中策略结果保存在p.eft 中。 4. Effect(效果):效果描述了当策略匹配请求时应该执行的操作,通常有两种效果:“允许”和“拒绝”。例如:e = some(where(p.eft == allow)) ,表示如果策略匹配结果理由有某些结果是“允许”,那么最终结果返回真。另一个例子:e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) ,这个组合的逻辑表示:如果有策略匹配结果为允许,且没有策略匹配结果是拒绝时,结果返回真。也就是说,当匹配的策略都是允许是,结果为真;如果有任何一个拒绝,则结果为假。
如下图,访问控制的过程通常是这样的:首先,用户发出一个请求,然后系统使用匹配器将请求与策略进行比较,如果请求匹配策略,那么将执行策略定义的效果(允许或拒绝)。
PERM模型的优点是它非常灵活,可以支持各种复杂的访问控制需求。同时,它也支持动态的访问控制,因为策略和匹配器都可以根据需要进行修改。 关于该模型的具体设计细节,可以参考原论文:PML: 基于解释器的Web服务访问控制策略语言[1]和基于元模型的访问控制策略规范语言(中文)[2] Casbin框架应用实践Casbin是一个强大的、高效的开源访问控制库,用于Golang、Java、Node.js、PHP等多种编程语言。它支持多种访问控制模型,如ACL(访问控制列表)、RBAC(基于角色的访问控制)和ABAC(基于属性的访问控制),可以帮助开发者轻松地实现对应用程序中资源的访问控制。 前面介绍的是各个权限模型的原理,以及Casbin开源框架的理论基础:PERM元模型。接下来的内容,我们讲介绍和使用Casbin框架来实现前面的几种权限模型。 下图是Casbin的原理说明: Casbin的ACL实现如下是一个ACL模型的定义: # Request definition [request_definition] r=sub, obj, act
# Policy definition [policy_definition] p=sub, obj, act
# Policy effect [policy_effect] e= some(where(p.eft == allow))
# Matchers [matchers] m= r.sub== p.sub&& r.obj == p.obj && r.act == p.act
· request_definition 是系统的查询模板。例如,请求 alice, write, data1 可以解释为 '主体 Alice 能否对对象'data1'执行'写'操作? · policy_definition 是系统的赋值模板。例如,通过创建 policy alice, write, data1,就等于为主体 Alice 分配了在对象 'data1 '上执行 '写 '操作的权限。 · policy_effect 定义了策略的效果。 · matchers使用 r.sub == p.sub && r.obj == p.obj && r.act == p.act 条件将请求与策略进行匹配。
下面举例一个策略来检测这个ACL模型: p, alice, data1, read p, bob, data2, write
这个策略表示: · alice可以读取data1 · bob可以编写data2
下图是ACL模型中policy和request的匹配过程:
上面的例子,我们通过casbin的golang库实现如下: 首先安装Casbin库: go get -u github.com/casbin/casbin/v2
接着,我们创建一个名为main.go 的文件,用于实现ACL权限模型。 package main
import( 'fmt'
'github.com/casbin/casbin/v2' 'github.com/casbin/casbin/v2/model' )
func main(){ // 1. 初始化一个Casbin的Enforcer,这里我们直接使用字符串来定义模型 m, _ := model.NewModelFromString(` [request_definition] r = sub, obj, act
[policy_definition] p = sub, obj, act
[policy_effect] e = some(where (p.eft == allow))
[matchers] m = r.sub == p.sub && r.obj == p.obj && r.act == p.act `)
enforcer, _ := casbin.NewEnforcer(m)
// 2. 添加策略 // 用户alice可以访问数据1的读操作 enforcer.AddPolicy('alice','data1','read') // 用户bob可以访问数据2的写操作 enforcer.AddPolicy('bob','data2','write')
// 3. 检查访问权限 // 检查alice是否可以访问data1的读操作 ok, _ := enforcer.Enforce('alice','data1','read') fmt.Printf('Alice can read data1: %v\n', ok)
// 检查bob是否可以访问data2的写操作 ok, _ = enforcer.Enforce('bob','data2','write') fmt.Printf('Bob can write data2: %v\n', ok)
// 检查alice是否可以访问data2的写操作 ok, _ = enforcer.Enforce('alice','data2','write') fmt.Printf('Alice can write data2: %v\n', ok) }
在这个示例中,我们首先定义了一个简单的ACL模型,然后添加了两条策略: 运行这个程序,你将看到以下输出: Alice can read data1: true Bob can write data2: true Alice can write data2: false
Casbin的RBAC实现Casbin 支持约束规则,这些规则用于在访问控制过程中对权限进行更细粒度的控制。约束规则的定义通常使用 c 标识符。 在 Casbin 中,约束规则的定义为: [policy_definition] c = _, _, constraint_name
其中,constraint_name 是约束的名称,代表对应的约束规则。现在,我来介绍一些常见的约束规则以及它们的示例: 1. 日期约束: 允许用户只在特定日期范围内访问某个资源。
[policy_definition] c = _, _, date
在这个例子中,date 可以是资源可以访问的日期范围,例如 2022-01-01 to 2022-12-31 。 1. 时间约束: 允许用户只在特定时间段内访问某个资源。
[policy_definition] c = _, _, time
在这个例子中,time 可以是资源可以访问的时间范围,例如 9:00 AM to 5:00 PM 。 1. 数量约束:
[policy_definition] c = _, _, max_operations
在这个例子中,max_operations 可以是用户对资源执行操作的最大次数,例如 10 。 1. 自定义约束:
允许用户定义自己的约束规则,例如根据用户属性、环境变量等条件限制访问。 [policy_definition] c = _, _, custom_constraint
在这个例子中,custom_constraint 是用户自定义的约束规则。 当 Casbin 执行权限检查时,会根据加载的策略和匹配的请求条件,结合约束规则来判断是否允许访问。实际使用时,约束规则的定义和语义是根据业务需求而定的,用户可以根据具体情况定义和使用约束规则。 我们设计一个简单的RBAC模型,如下: [request_definition] r =sub, act, obj
[policy_definition] p =sub, act, obj
[role_definition] g = _, _ g2 = _, _
[policy_effect] e = some(where(p.eft == allow))
[matchers] m = r.sub== p.sub&& g(p.act, r.act)&& r.obj == p.obj
我们创建一个名为main.go 的文件,用于实现RBAC权限模型。 package main
import( 'fmt'
'github.com/casbin/casbin/v2' 'github.com/casbin/casbin/v2/model' )
func main(){ // 1. 初始化一个Casbin的Enforcer,这里我们直接使用字符串来定义模型 m, _ := model.NewModelFromString(` [request_definition] r = sub, obj, act
[policy_definition] p = sub, obj, act
[role_definition] g = _, _
[policy_effect] e = some(where (p.eft == allow))
[matchers] m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`)
enforcer, _ := casbin.NewEnforcer(m)
// 2.定义角色的策略 //admin角色可以访问data1的读写操作 enforcer.AddPolicy('admin','data1','read') enforcer.AddPolicy('admin','data1','write') // member角色只可以访问data2的读操作 enforcer.AddPolicy('member','data2','read')
// 3. 给用户添加角色 // 用户alice拥有admin角色 enforcer.AddGroupingPolicy('alice','admin') // 用户bob拥有member角色 enforcer.AddGroupingPolicy('bob','member')
// 4. 检查访问权限 // 检查alice是否可以访问data1的读操作 ok, _ := enforcer.Enforce('alice','data1','read') fmt.Printf('Alice can read data1: %v\n', ok)
// 检查alice是否可以访问data1的写操作 ok, _ = enforcer.Enforce('alice','data1','write') fmt.Printf('Alice can write data1: %v\n', ok)
// 检查bob是否可以访问data2的读操作 ok, _ = enforcer.Enforce('bob','data2','read') fmt.Printf('Bob can read data2: %v\n', ok)
// 检查bob是否可以访问data1的写操作 ok, _ = enforcer.Enforce('bob','data1','write') fmt.Printf('Bob can write data1: %v\n', ok) }
下面是这个代码示例的解释: 1. 首先,我们使用字符串定义了一个RBAC模型。在这个模型中,我们定义了请求(r = sub, obj, act ),策略(p = sub, obj, act ),角色的定义(g = _, _ ),策略效果(e = some(where (p.eft == allow)) )和匹配表达式(m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act )。 2. 然后,我们初始化了一个Casbin的Enforcer。 3. 添加了角色和策略。我们将alice添加到admin角色,将bob添加到member角色。然后定义admin角色可以对data1进行读写操作,member角色只可以对data2进行读操作。 4. 使用Enforce 方法检查用户的访问权限。在这个示例中,我们检查了alice和bob对data1和data2的读写权限。
运行这个程序,你将看到以下输出: Alice can read data1: true Alice can write data1: true Bob can read data2: true Bob can write data1: false
Casbin的ABAC实现我们创建一个名为main.go 的文件,用于实现ABAC权限模型。 package main
import( 'fmt'
'github.com/casbin/casbin/v2' 'github.com/casbin/casbin/v2/model' )
// User 定义用户属性 typeUserstruct{ Namestring Ageint Rolestring }
// Resource 定义资源属性 typeResourcestruct{ Namestring Locationstring }
// 定义一个自定义的策略函数 func policyFunc(args ...interface{})(interface{},error){ user := args[0].(*User) resource := args[1].(*Resource) action := args[2].(string)
// 如果用户角色是admin,或者用户角色是owner并且资源在USA,或者用户年龄大于18并且操作是read,则允许访问 if user.Role=='admin'||(user.Role=='owner'&& resource.Location=='USA')||(user.Age>18&& action =='read'){ returntrue,nil }
returnfalse,nil }
func main(){ // 使用字符串定义模型 m, _ := model.NewModelFromString(` [request_definition] r = sub, obj, act
[policy_definition] p = sub, obj, act
[policy_effect] e = some(where (p.eft == allow))
[matchers] m = PolicyFunc(r.sub, r.obj, r.act) `)
// 初始化一个Casbin的Enforcer enforcer, _ := casbin.NewEnforcer(m)
// 添加自定义策略函数 enforcer.AddFunction('PolicyFunc', policyFunc)
// 创建用户和资源 userAlice :=&User{Name:'Alice',Age:20,Role:'admin'} userBob :=&User{Name:'Bob',Age:16,Role:'member'} resourceData1 :=&Resource{Name:'data1',Location:'USA'}
// 检查访问权限 ok, _ := enforcer.Enforce(userAlice, resourceData1,'read') fmt.Printf('Alice can read data1: %v\n', ok)
ok, _ = enforcer.Enforce(userBob, resourceData1,'read') fmt.Printf('Bob can read data1: %v\n', ok)
ok, _ = enforcer.Enforce(userBob, resourceData1,'write') fmt.Printf('Bob can write data1: %v\n', ok) }
在上面的代码中,我们实现了一个基于Casbin的ABAC(基于属性的访问控制)权限模型。下面是对代码的详细解释: 1. 首先,我们定义了两个结构体:User 和 Resource 。User 结构体包含了用户的属性(名称、年龄、角色),Resource 结构体包含了资源的属性(名称、位置)。 2. 接下来,我们定义了一个自定义的策略函数 policyFunc 。这个函数接受三个参数:用户、资源和操作。根据这些参数,函数判断用户是否有权限访问资源。在这个示例中,我们定义了以下规则: 3. 初始化一个Casbin的Enforcer,并使用 AddFunction 方法将自定义策略函数添加到 Enforcer 中。 4. 创建用户和资源实例:userAlice 、userBob 和 resourceData1 。 5. 使用 Enforce 方法检查用户对资源的访问权限。在这个示例中,我们检查了 Alice 和 Bob 对 data1 资源的读写权限。
运行这个程序,你将看到以下输出: Alice can read data1: true Bob can read data1: false Bob can write data1: false
Casbin综合实践表结构定义接下来,我们通过go的casbin库来完整实现一个RBAC权限模型,通过在API服务器中调用RBAC模型进行权限校验。 首先,创建MySQL表结构: -- 用户表 CREATETABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50)UNIQUENOTNULL, password VARCHAR(50)NOTNULL );
-- 角色表 CREATETABLE roles ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50)UNIQUENOTNULL );
-- 权限表 CREATETABLE permissions ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50)UNIQUENOTNULL, menu VARCHAR(50)NOTNULL );
在这里,用户表(users)存储用户信息,角色表(roles)存储角色信息,权限表(permissions)存储权限信息。role_constraints 表用于存储角色的约束信息。 当使用Casbin库存储策略到数据库时,Casbin会创建一个规则表用于存储策略信息。这个表的结构取决于具体的数据库适配器。对于Gorm适配器(gorm-adapter ),Casbin将创建一个表,通常命名为casbin_rule ,用于存储策略规则。实际在开发的时候,用户角色关联表(user_roles)、角色权限关联信息(role_permissions)这个三个表可以不用创建,直接复用下面的casbin_rule表。所以实际开发的时候,只需要用户表(users)、角色表(roles)、约束表(role_constraints)和规则表(casbin_rule)四张表即可。 以下是一个简化的Casbin规则表结构的例子: CREATE TABLE casbin_rule ( id INT AUTO_INCREMENT PRIMARY KEY, ptype VARCHAR(100), v0 VARCHAR(100), v1 VARCHAR(100), v2 VARCHAR(100), v3 VARCHAR(100), v4 VARCHAR(100), v5 VARCHAR(100) );
解释表结构中的字段: · id : 规则的唯一标识,通常是一个自增的整数。 · ptype : 策略类型,通常是'p'表示权限策略(policy)或'g'表示角色关联策略(grouping policy)或角色继承。 · v0, v1, v2, v3, v4, v5 : 规则的各个参数值,这些参数值的含义依赖于策略类型。 · 对于权限策略,通常是(sub, obj, act)三元组。 · 对于角色关联策略,通常是用户与角色之间的关联。 · 对于角色继承,通常是子角色和父角色之间的关系。
例如,一条权限策略规则可能如下,这表示用户'alice'具有对资源'data1'执行'read'操作的权限。 INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'alice', 'data1', 'read');
而一条角色关联策略规则可能如下,这表示用户'alice'拥有角色'admin'。 INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g', 'alice', 'admin');
而角色继承规则可能如下,这条语句表示'user'角色继承'admin'角色。'g'表示这是一个角色继承关系,v0是父角色,v1是子角色。 INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g', 'admin', 'user');
权限模型定义然后,初始化Casbin的RBAC模型配置文件(rbac_model.conf)如下: # rbac_model.conf
# 定义请求的匹配规则 [request_definition] r=sub, obj, act
# 定义策略规则 [policy_definition] p=sub, obj, act
# 定义策略效果 [policy_effect] e= some(where(p.eft == allow))
# 定义匹配器规则 [matchers] m= g(r.sub, p.sub)&& keyMatch(r.obj, p.obj)&&(r.act == p.act || p.act =='*')
# 角色继承规则的默认效果 [role_definition] g= _, _
这个文件的解释如下: · [request_definition] :定义了一个请求需要的元素。在这里,请求需要三个元素:主体(sub,表示用户)、对象(obj,表示资源)和操作(act,表示对资源的操作,如读、写等)。 · [policy_definition] :定义了策略规则需要的元素。与请求定义相同,策略规则也需要三个元素:主体、对象和操作。 · [policy_effect] :定义了策略效果。在这里,策略效果是“some(where (p.eft == allow))”,表示只要有一个策略规则允许访问,请求就被允许。 · [matchers] :定义了匹配器规则。匹配器规则用于确定请求和策略规则是否匹配。在这里,匹配器规则包括三部分: · g(r.sub, p.sub) :表示请求的主体需要具有策略规则中定义的角色。 · keyMatch(r.obj, p.obj) :表示请求的对象需要与策略规则中的对象匹配。keyMatch 是Casbin内置的匹配函数,支持部分匹配和通配符。 · (r.act == p.act || p.act == '*') :表示请求的操作需要与策略规则中的操作匹配,或者策略规则中的操作是通配符(表示允许所有操作)。
· [role_definition] :定义了角色继承规则。在这里,角色继承规则是g = _, _ ,表示角色可以继承其他角色。这在实际应用中很有用,例如,你可以定义一个管理员角色,继承了所有普通用户角色的权限。
源码当使用 Go 中的 HTTP 中间件时,你可以创建一个中间件函数,该函数在每个请求到达 HTTP 处理器之前执行权限校验。 rbac.go文件
package main
import( 'fmt' 'log' 'net/http'
'github.com/casbin/casbin/v2' gormadapter 'github.com/casbin/gorm-adapter/v3' 'gorm.io/driver/mysql' 'gorm.io/gorm' )
// 数据库连接信息 const( DBUsername='root' DBPassword='123' DBHost='localhost' DBPort='3306' DBName='rbac' )
// 初始化 Casbin Enforcer func InitCasbinEnforcer()(*casbin.Enforcer,error){ dsn := fmt.Sprintf('%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local',DBUsername,DBPassword,DBHost,DBPort,DBName) db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{}) if err !=nil{ returnnil, err }
adapter, err := gormadapter.NewAdapterByDB(db) if err !=nil{ returnnil, err }
enforcer, err := casbin.NewEnforcer('rbac_model.conf', adapter) if err !=nil{ returnnil, err }
err = enforcer.LoadPolicy() if err !=nil{ returnnil, err }
return enforcer,nil }
// 鉴权 func Authorizer(e *casbin.Enforcer, username string)func(next http.Handler) http.Handler{ returnfunc(next http.Handler) http.Handler{ fn :=func(w http.ResponseWriter, r *http.Request){ role :=GetRole(username) if role ==''{ role ='anonymous' }
// casbin rule enforcing res, err := e.Enforce(role, r.URL.Path, r.Method) if err !=nil{ http.Error(w,'[1] Internal Server Error', http.StatusInternalServerError) return } if res { next.ServeHTTP(w, r) }else{ http.Error(w, fmt.Sprintf('[2] Access Forbidden(%s,%s,%s)', role, r.URL.Path, r.Method), http.StatusForbidden) return } }
return http.HandlerFunc(fn) } }
// 获取用户拥有的角色 func GetRole(username string)(role string){ // 初始化数据库连接 dsn := fmt.Sprintf('%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local',DBUsername,DBPassword,DBHost,DBPort,DBName) db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{}) if err !=nil{ return }
var arr []string db.Raw('select v1 from casbin_rule where v0=? and ptype=?', username,'g').Scan(&arr) iflen(arr)>0{ role = arr[0] }
return }
// 获取用户拥有的所有权限(包括继承角色的) func GetPermissions(username string, enforcer *casbin.Enforcer)(permissions []string){ arr, _ := enforcer.GetImplicitPermissionsForUser(username)
// arr结构:[['admin','/*','*'],['anonymous','/login','*'],['member','/logout','*'],['member','/member/*','*']] // 数组组成是角色、资源、操作 visited :=make(map[string]bool) for _, policy :=range arr { if _, ok := visited[policy[1]];!ok { visited[policy[1]]=true permissions =append(permissions, policy[1]) } } return }
// 获取用户拥有的菜单按钮权限 func GetMenus(username string, enforcer *casbin.Enforcer)(menus []string){ // 获取用户拥有的所有权限(包括继承角色的) arr, _ := enforcer.GetImplicitPermissionsForUser(username) var permissions []string
// arr结构:[['admin','/*','*'],['anonymous','/login','*'],['member','/logout','*'],['member','/member/*','*']] // 数组组成是角色、资源、操作 for _, policy :=range arr { permissions =append(permissions, policy[1]) }
// 初始化数据库连接 dsn := fmt.Sprintf('%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local',DBUsername,DBPassword,DBHost,DBPort,DBName) db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{}) if err !=nil{ return }
iflen(permissions)==0{ return } db.Raw('select distinct menu from permissions where name in ?', permissions).Scan(&menus)
return }
func logoutHandler() http.HandlerFunc{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ w.Write([]byte('Logout success\n')) }) }
func loginHandler() http.HandlerFunc{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ w.Write([]byte('Login success\n')) }) }
func currentMemberHandler() http.HandlerFunc{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ w.Write([]byte(fmt.Sprintf('Get current member success:%s\n', r.URL.Path))) }) }
func adminHandler() http.HandlerFunc{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ w.Write([]byte('I'm an Admin!\n'))
}) }
func main(){ // 初始化 Casbin Enforcer enforcer, err :=InitCasbinEnforcer() if err !=nil{ log.Fatal('Failed to initialize Casbin enforcer:', err) }
username :='alice'// 假设当前用户 // 获取用户所有角色(包括角色继承的) roles, _ := enforcer.GetImplicitRolesForUser(username) log.Println(fmt.Sprintf('%s has these roles:%v', username, roles))
// 获取用户所拥有的权限 permissions :=GetPermissions(username, enforcer) log.Println(fmt.Sprintf('%s has these permissions:%v', username, permissions))
// 获取用户所拥有的菜单权限 menus :=GetMenus(username, enforcer) log.Println(fmt.Sprintf('%s has these menus:%v', username, menus))
mux := http.NewServeMux() mux.HandleFunc('/login', loginHandler()) mux.HandleFunc('/logout', logoutHandler()) mux.HandleFunc('/member/lisi', currentMemberHandler()) mux.HandleFunc('/member/zhangsan', currentMemberHandler()) mux.HandleFunc('/admin/stuff', adminHandler())
// 启动API服务器 log.Println('Server is running on :8080') log.Fatal(http.ListenAndServe(':8080',Authorizer(enforcer, username)(mux)))
}
测试为了测试RBAC模型,我们初始化一些测试数据。 首先是初始化一些用户、角色和权限表。 -- 初始化用户 INSERTINTO users(username,passowrd)VALUES('tom','pwd1'); INSERTINTO users(username,passowrd)VALUES('alice','pwd1'); INSERTINTO users(username,passowrd)VALUES('bob','pwd1');
-- 初始化角色 INSERTINTO roles(name)VALUES('admin'); INSERTINTO roles(name)VALUES('anonymous'); INSERTINTO roles(name)VALUES('member');
-- 初始化权限表 INSERTINTO permissions(name, menu)VALUES('/login','用户模块'); INSERTINTO permissions(name, menu)VALUES('/logout','用户模块'); INSERTINTO permissions(name, menu)VALUES('/member/*','成员模块'); INSERTINTO permissions(name, menu)VALUES('/admin/staff','管理员模块');
因为我们要关联接口对应的菜单关系,所以在permissions表里维护接口菜单关系时,接口名称要具体到完整路径,不能用类似/* 这种模糊匹配。 然后在设计策略的时候,角色关联的接口权限也要具体到完整接口路径,这个后面可以和前面的permissions表里根据接口地址获取到对应的菜单列表,前端就可以根据菜单列表做控制展示。 下面的策略规则表示如下: p, admin,/*, * p, admin, /login, * p, admin, /logout, * p, admin, /member/*, * p, admin, /admin/staff, *
p, anonymous, /login, *
p, member, /login, * p, member, /logout, * p, member, /member/*, *
g, tom, admin g, alice, member g, bob, member
g, member, anonymous g, admin, member
INSERT INTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/*','*'); INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/login','*'); INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/logout','*'); INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/member/*','*'); INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/admin/staff','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','anonymous','/login','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','member','/login','*'); INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','member','/logout','*'); INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','member','/member/*','*');
然后我们给3个用户初始化对应的角色,并且让member角色继承anonymous角色,也就是说member角色也包含anonymous角色的权限,同样的admin角色继承了member角色,也就间接继承了member角色和anonymous角色的所有权限。 INSERT INTO casbin_rule (ptype, v0, v1)VALUES('g','tom','admin'); INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','alice','member'); INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','bob','anonymous');
INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','member','anonymous'); INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','admin','member');
前面代码,我们假设请求的当前用户是alice,启动前面的代码,我们可以看到输出如下,可以看到alice拥有的角色member,同时因为member是继承anonymous角色,所以也间接拥有anonymous角色。最终可以看到alice拥有member和anonymous两个角色的接口权限。 go run rbac.go
2023/xx/xx 17:20:52 alice has these roles:[member anonymous] 2023/xx/xx 17:20:52 alice has these permissions:[/login /logout /member/*] 2023/xx/xx 17:20:52 alice has these menus:[用户模块 成员模块]
实际去请求下面4个接口,返回是通过的,跟策略一样。 请求:curl -d '{}' http://localhost:8080/login 响应:Login success
请求:curl -d '{}' http://localhost:8080/logout 响应:Logout success
请求:curl -d '{}' http://localhost:8080/member/lisi 响应:Get current member success:/member/lisi
请求:curl -d '{}' http://localhost:8080/member/zhangsan 响应:Get current member success:/member/zhangsan
当请求admin角色的接口时,就收到无权限的提示,符合预期设置的策略规则。 请求:curl -d '{}' http://localhost:8080/admin/staff 响应:[2] Access Forbidden(role:member,/admin/staff,POST)
假设当前用户是bob,他是anonymous角色,执行go run rbac.go 输出如下: 2023/xx/xx 17:24:08 bob has these roles:[anonymous] 2023/xx/xx 17:24:08 bob has these permissions:[/login] 2023/xx/xx 17:24:08 bob has these menus:[用户模块]
假设当前用户是tom,他是admin角色,执行go run rbac.go 输出如下: 2023/xx/xx 17:24:48 tom has these roles:[admin member anonymous] 2023/xx/xx 17:24:48 tom has these permissions:[/login /logout /member/* /admin/staff] 2023/xx/xx 17:24:48 tom has these menus:[管理员模块 用户模块 成员模块]
参考[1] https://www./infs767/infs767fall03/lecture01-2.pdf [2] https://www./infs767/infs767fall03/ [3] https://www.cnblogs.com/wang_yb/archive/2018/11/20/9987397.html [4] https:///pdf/1903.09756.pdf [5] https:///zh/docs/tutorials [6] https://narendraj9./posts/generalized-authz.html [7] https://www./jos/article/abstract/5624
|