权限系统,例如: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--”。这个字符串从左到右分为四部分:
- 第一个字符表示文件类型:'-'表示普通文件,'d'表示目录,'l'表示符号链接等。
- 接下来的三个字符表示文件所有者的权限:'r'表示可读,'w'表示可写,'x'表示可执行。没有某个权限的话,对应的位置会被标为'-'。
- 再接下来的三个字符表示文件所属用户组的权限,同样用'r'、'w'和'x'表示。
- 最后三个字符表示其他用户的权限,也是用'r'、'w'和'x'表示。
例如,下面的resource_2文件ACL字符串为“-rw-r-----”,表示:
- 这是一个普通文件(-)。
- 文件所有者具有读(r)和写(w)权限。
- 文件所属的用户组具有读(r)权限。
- 其他用户没有任何权限。
[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提供了一种灵活的权限管理机制,但在实际应用中,它也存在一些问题,例如:
- 管理复杂性:当系统中的资源和用户数量较大时,管理大量的ACL可能变得非常复杂。为每个文件或资源分配权限可能会导致管理负担加重,尤其是当需要修改或更新权限时。
- 难以追踪和审计:由于权限是分散在各个资源的ACL中的,追踪和审计用户的访问权限可能变得困难。例如,要查找具有特定权限的所有用户,可能需要检查所有资源的ACL。
- 权限维护困难:当用户的角色或职责发生变化时,可能需要修改多个ACL以更新用户的权限。这不仅耗时,而且容易出错。
- 缺乏抽象和封装:ACL直接将权限分配给用户或用户组,缺乏对权限的抽象和封装。这可能导致权限管理变得繁琐和低效。
RBAC(Role-Based Access Control,基于角色的访问控制)模型通过引入角色的概念来解决这些问题,使得权限管理更加简单、高效和可维护。在RBAC模型中,权限不再直接分配给用户,而是分配给角色。用户根据需要分配一个或多个角色,从而间接获得角色所包含的权限。
以下是RBAC模型的关键组成部分:
- 用户(Users):用户是系统中的实体,如人员、服务或应用程序。用户需要访问系统资源以完成特定任务。
- 角色(Roles):角色是一组相关权限的集合,通常与特定职责或职位相对应。例如,管理员、普通用户和访客等。角色应该具有明确的职责和权限范围,遵循最小权限原则,即只分配角色所需的最小权限。
- 权限(Permissions):权限是对资源的访问或操作的授权。例如,对文件的读、写和删除权限。在RBAC模型中,权限分配给角色,而不是直接分配给用户。
- 资源(Resources):资源是系统中需要受到保护的对象,如文件、数据库表、服务等。资源可以根据需要分配给角色,以控制用户对资源的访问和操作。
RBAC的优点
通过以上步骤,可以实现基于RBAC模型的权限设计,这个模型可以弥补前面ACL模型的各种缺陷,具体来说就是有以下这些优势:
- 简化管理:通过将权限分配给角色而非直接分配给用户,RBAC模型简化了权限管理。管理员只需要管理角色和用户与角色之间的关系,而不是为每个用户分配权限。
- 更易于追踪和审计:在RBAC模型中,权限集中在角色中,更容易追踪和审计用户的访问权限。例如,要查找具有特定权限的所有用户,只需检查与该权限相关的角色,然后查找分配了这些角色的用户。
- 权限维护更简单:当用户的角色或职责发生变化时,只需修改用户的角色分配,而无需逐个修改资源的ACL。这使得权限维护更加简单和高效。
- 权限抽象和封装: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模型中角色管理的主要步骤:
- 角色定义:首先,需要定义系统中的角色。角色通常对应于组织中的职位或职责,如“管理员”、“编辑”、“访客”等。每个角色都应该有一个明确的职责和权限范围。
- 权限分配:为每个角色分配一组相应的权限。权限通常是对系统资源的访问或操作的授权,如“读”、“写”、“删除”等。
- 用户与角色关联:将用户分配到一个或多个角色。用户通过角色获得访问系统资源的权限。
- 角色修改和删除:当组织或业务需求发生变化时,可能需要修改或删除角色。例如,当一个角色的职责发生变化时,可能需要修改该角色的权限;当一个角色不再需要时,可以删除该角色。
在实际应用中,角色管理通常需要配合用户管理和权限管理一起使用。例如,当新用户加入系统时,需要为其分配适当的角色;当系统资源或权限策略发生变化时,可能需要更新角色的权限。
在进行角色管理时,需要采取如下的安全原则进行合理设计来提高系统安全性:
- 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小的权限集合
- 责任分离原则:可以通过调用相互独立互斥的角色共同完成敏感的任务,例如要求一个记账员和财务管理员共同参与统一过账操作
- 数据抽象原则:可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读、写等执行权限
# 用户管理
用户管理主要做两方面工作流:
- 确定用户在哪个组织
- 确定用户在这个组织里有哪些权限(即绑定什么角色)
# RBAC的四个层次
在RBAC模型中,为了更好地描述和理解不同程度的角色管理和访问控制,研究者将RBAC模型分为四个层次:RBAC0、RBAC1、RBAC2和RBAC3。这些层次从简单到复杂,逐步增加了角色管理和访问控制的功能。
# RBAC0
- RBAC0:RBAC0是RBAC模型的最基本层次,它包括用户(User)、角色(Role)和权限(Permission)三个基本元素。在RBAC0中,用户通过分配角色来获得权限。RBAC0模型关注于将权限分配给角色,以及将角色分配给用户。它不涉及角色之间的关系,也不包括角色继承。这种在大部分系统上是最常见的设计,可以满足绝大部分系统的权限模块设计需求。
# RBAC1
RBAC1:RBAC1在RBAC0的基础上引入了角色继承(Role Hierarchy)机制。角色继承允许一个角色继承另一个角色的权限。这种层次化的角色结构有助于简化权限管理,使权限分配更加结构化和模块化。
角色继承的主要特点如下:
- 层次化:角色继承允许创建具有层次结构的角色。在这种结构中,一个角色可以继承一个或多个父角色的所有权限。这有助于组织和管理角色,使权限分配更加结构化和模块化。
- 权限累积:当一个角色继承另一个角色时,它将自动获得被继承角色的所有权限。这意味着在分配权限时,只需为每个角色分配特定的权限,而无需重复分配共享的权限。
- 灵活性:角色继承提供了一种灵活的方式来管理和分配权限。当需要调整权限时,只需修改角色继承关系,而无需逐个修改用户的权限。这使得权限管理更加灵活和高效。
举个例子,假设我们有以下角色:
- 一般员工(Employee)
- 经理(Manager)
- 系统管理员(System Administrator)
在这个例子中,我们可以使用角色继承来实现以下权限分配:
- 一般员工(Employee)具有基本的访问权限,如查看文件、发送电子邮件等。
- 经理(Manager)继承一般员工(Employee)的所有权限,并具有额外的权限,如审批报告、管理项目等。
- 系统管理员(System Administrator)继承经理(Manager)的所有权限,并具有额外的权限,如管理用户、配置系统等。
# RBAC2
RBAC2:RBAC2在RBAC1的基础上引入了约束(Constraint)机制。约束用于限制用户与角色、角色与权限之间的关联,以增强访问控制的安全性和灵活性。约束可以是静态的(例如,一个用户最多可以分配两个角色),也可以是动态的(例如,一个用户不能同时拥有某两个互斥的角色)。
约束可以分为静态约束和动态约束,以下是一些常见的RBAC2约束类型及示例:
互斥角色(Mutually Exclusive Roles):互斥角色约束要求一个用户不能同时拥有某些特定的角色。这可以防止潜在的冲突或滥用权限。例如,在一个银行系统中,一个用户可能不能同时拥有“出纳员”和“审计员”的角色,以防止内部欺诈。
基数约束(Cardinality Constraint):基数约束限制了分配给用户的角色数量或分配给角色的权限数量。这有助于遵循最小权限原则,降低潜在的安全风险。例如,一个用户最多可以分配两个角色;或者一个角色最多可以拥有五个权限。
权责分离约束(Separation of Duties,SoD):权责分离约束要求将潜在冲突的任务分配给不同的角色。这有助于防止滥用权限和内部欺诈。例如,在一个采购系统中,“采购申请者”和“采购审批者”应该是两个独立的角色,以确保采购过程的透明和公正。
前置角色约束(Prrequisite Roles Constraint):只有当用户已是角色 B 的成员时,才能将其分配给角色 A。例如要经历过主管的角色之后,才能晋级总监角色。
在实际应用中,可以根据具体的业务需求和安全策略来定义和实施RBAC2约束。这些约束可以通过编程逻辑或数据库规则等方式来实现。
# RBAC3
RBAC3 = 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模型简化了权限管理,提高了安全性和可维护性,但它也存在一些缺点或局限性:
- 静态角色:RBAC模型中的角色通常是预先定义好的,这可能导致在面对复杂和动态的访问控制需求时,灵活性较低。例如,如果需要根据时间、地点或其他上下文信息来控制访问权限,RBAC模型可能难以满足需求。
- 角色爆炸:在某些场景下,为了满足细粒度的访问控制需求,可能需要创建大量的角色,这会导致角色管理变得复杂,也称为“角色爆炸”。
- 缺乏属性支持:RBAC模型主要依赖于角色来控制访问权限,而不支持基于用户、资源或环境等属性的访问控制。这可能导致在需要支持基于属性的访问控制的场景中,RBAC模型难以满足需求。
ABAC(Attribute-Based Access Control,基于属性的访问控制)模型正是为了解决这些问题而提出的。ABAC模型允许基于用户、资源、操作和环境等多个属性来控制访问权限。这些属性可以是静态的(例如用户的部门、资源的类型)或动态的(例如当前时间、用户的位置)。
ABAC模型通过引入属性和动态访问控制策略,解决了RBAC模型在复杂和动态访问控制场景中的缺点和局限性,使得权限管理更加灵活和高效。然而,实现ABAC模型可能相对复杂,需要对属性和策略进行管理和维护。
# PERM元模型
PERM(Policy, Effect, Request, Matchers)建模语言(PERM modeling language, 简称PML),是一种用于访问控制的模型。模型中的每个元素都有其特定的含义和作用:
Request(请求):请求是用户试图执行的操作,通常一个基础的请求是一个三元组,包括用户身份(subject)、目标资源(object)和操作类型(action)信息,即:
r = {sub, obj, act}
。例如,用户试图删除一个文件,这就构成了一个请求。Policy(策略):策略是定义谁可以执行哪些操作,或者在哪些条件下可以访问哪些资源的规则,即:
p = {sub, obj, act}或p={sub, obj, act, eft}
。例如,策略可能会规定“管理员可以删除所有文件”,或者“只有在工作时间内,员工才能访问公司数据库”。Matchers(匹配器):匹配器是用来比较请求(Request)和策略(Policy)的工具,如果请求和策略匹配,那么将执行策略定义的效果,匹配器可以是简单的比较操作,也可以是复杂的逻辑表达式。例如:
m = r.sub == p.sub && r.action == p.action && r.resource == p.resource
,这个简单的匹配规则表示,如果请求的参数(subject, object, action)
能在策略中找到对应的匹配结果,则返回策略结果p.eft
,其中策略结果保存在p.eft
中。Effect(效果):效果描述了当策略匹配请求时应该执行的操作,通常有两种效果:“允许”和“拒绝”。
例如:
e = some(where(p.eft == allow))
,表示如果策略匹配结果理由有某些结果是“允许”,那么最终结果返回真。另一个例子:
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
,这个组合的逻辑表示:如果有策略匹配结果为允许,且没有策略匹配结果是拒绝时,结果返回真。也就是说,当匹配的策略都是允许是,结果为真;如果有任何一个拒绝,则结果为假。
如下图,访问控制的过程通常是这样的:首先,用户发出一个请求,然后系统使用匹配器将请求与策略进行比较,如果请求匹配策略,那么将执行策略定义的效果(允许或拒绝)。
PERM模型的优点是它非常灵活,可以支持各种复杂的访问控制需求。同时,它也支持动态的访问控制,因为策略和匹配器都可以根据需要进行修改。
关于该模型的具体设计细节,可以参考原论文:PML: 基于解释器的Web服务访问控制策略语言 (opens new window)和基于元模型的访问控制策略规范语言(中文) (opens new window)
# 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可以访问data1的读操作
bob可以访问data2的写操作。
最后,我们检查了用户的访问权限。
运行这个程序,你将看到以下输出:
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
是约束的名称,代表对应的约束规则。现在,我来介绍一些常见的约束规则以及它们的示例:
- 日期约束: 允许用户只在特定日期范围内访问某个资源。
[policy_definition]
c = _, _, date
在这个例子中,date
可以是资源可以访问的日期范围,例如 2022-01-01 to 2022-12-31
。
- 时间约束: 允许用户只在特定时间段内访问某个资源。
[policy_definition]
c = _, _, time
在这个例子中,time
可以是资源可以访问的时间范围,例如 9:00 AM to 5:00 PM
。
- 数量约束:
[policy_definition]
c = _, _, max_operations
在这个例子中,max_operations
可以是用户对资源执行操作的最大次数,例如 10
。
- 自定义约束:
允许用户定义自己的约束规则,例如根据用户属性、环境变量等条件限制访问。
[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)
}
下面是这个代码示例的解释:
- 首先,我们使用字符串定义了一个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
)。 - 然后,我们初始化了一个Casbin的Enforcer。
- 添加了角色和策略。我们将alice添加到admin角色,将bob添加到member角色。然后定义admin角色可以对data1进行读写操作,member角色只可以对data2进行读操作。
- 使用
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 定义用户属性
type User struct {
Name string
Age int
Role string
}
// Resource 定义资源属性
type Resource struct {
Name string
Location string
}
// 定义一个自定义的策略函数
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") {
return true, nil
}
return false, 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(基于属性的访问控制)权限模型。下面是对代码的详细解释:
- 首先,我们定义了两个结构体:
User
和Resource
。User
结构体包含了用户的属性(名称、年龄、角色),Resource
结构体包含了资源的属性(名称、位置)。 - 接下来,我们定义了一个自定义的策略函数
policyFunc
。这个函数接受三个参数:用户、资源和操作。根据这些参数,函数判断用户是否有权限访问资源。在这个示例中,我们定义了以下规则:- 如果用户是 admin,允许访问;
- 如果用户是 owner,并且资源位于 USA,允许访问;
- 如果用户年龄大于 18,并且操作是 read,允许访问。
- 初始化一个Casbin的Enforcer,并使用
AddFunction
方法将自定义策略函数添加到 Enforcer 中。 - 创建用户和资源实例:
userAlice
、userBob
和resourceData1
。 - 使用
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表结构:
-- 用户表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL
);
-- 角色表
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
-- 权限表
CREATE TABLE permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
menu VARCHAR(50) NOT NULL
);
在这里,用户表(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 {
return nil, err
}
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
return nil, err
}
enforcer, err := casbin.NewEnforcer("rbac_model.conf", adapter)
if err != nil {
return nil, err
}
err = enforcer.LoadPolicy()
if err != nil {
return nil, err
}
return enforcer, nil
}
// 鉴权
func Authorizer(e *casbin.Enforcer, username string) func(next http.Handler) http.Handler {
return func(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)
if len(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
}
if len(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模型,我们初始化一些测试数据。
首先是初始化一些用户、角色和权限表。
-- 初始化用户
INSERT INTO users(username,passowrd) VALUES ('tom', 'pwd1');
INSERT INTO users(username,passowrd) VALUES ('alice', 'pwd1');
INSERT INTO users(username,passowrd) VALUES ('bob', 'pwd1');
-- 初始化角色
INSERT INTO roles(name) VALUES ('admin');
INSERT INTO roles(name) VALUES ('anonymous');
INSERT INTO roles(name) VALUES ('member');
-- 初始化权限表
INSERT INTO permissions(name, menu) VALUES ('/login', '用户模块');
INSERT INTO permissions(name, menu) VALUES ('/logout', '用户模块');
INSERT INTO permissions(name, menu) VALUES ('/member/*', '成员模块');
INSERT INTO permissions(name, menu) VALUES ('/admin/staff', '管理员模块');
因为我们要关联接口对应的菜单关系,所以在permissions表里维护接口菜单关系时,接口名称要具体到完整路径,不能用类似/*
这种模糊匹配。
然后在设计策略的时候,角色关联的接口权限也要具体到完整接口路径,这个后面可以和前面的permissions表里根据接口地址获取到对应的菜单列表,前端就可以根据菜单列表做控制展示。
下面的策略规则表示如下:
- admin角色拥有所有接口权限
- anonymous角色只有登录接口权限
- member角色可以登录、退出以及访问/member/路径的接口权限
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', '/*', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'admin', '/login', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'admin', '/logout', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'admin', '/member/*', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'admin', '/admin/staff', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'anonymous', '/login', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'member', '/login', '*');
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'member', '/logout', '*');
INSERT INTO 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');
INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g', 'alice', 'member');
INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g', 'bob', 'anonymous');
INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g', 'member', 'anonymous');
INSERT INTO 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.profsandhu.com/infs767/infs767fall03/lecture01-2.pdf
[2] https://www.profsandhu.com/infs767/infs767fall03/
[3] https://www.cnblogs.com/wang_yb/archive/2018/11/20/9987397.html
[4] https://arxiv.org/pdf/1903.09756.pdf
[5] https://casbin.org/zh/docs/tutorials
[6] https://narendraj9.github.io/posts/generalized-authz.html