文章目录
- 一、Casbin 是什么?
- 二、快速开始
- 2.1 载入配置
- 2.2 如何判断权限
- 2.3 model.conf
- 2.3.1 基本格式
- 2.3.2 SpringBoot下的使用
- 2.3.3 匹配的函数
- 内置函数
- 自定义函数
- 2.3.4 基于角色的访问控制
- 角色的层次
- 区分用户和角色
- 隐式角色权限
- 域内RBAC
- 角色与函数
- 2.3.5 优先级模型
- 通过隐式优先级加载策略
- 通过显式优先级加载策略
- 基于角色和用户层次结构以优先级加载策略
- 2.3.6 超级管理员
- 2.4 存储操作
- 2.4.1 Model存储
- 2.4.2 Policy存储
- 从 CSV 文件载入策略
- 数据库存储格式
- 适配器
- 政策子集加载
- 三、扩充功能部分
- 四、API部分
- 五、其他
- 多数据源
- 异常设定
- 附录
一、Casbin 是什么?
也被成为PERM模型: subject(sub 访问实体),object(obj访问的资源)和action(act访问方法)eft(策略结果,一般为空 默认指定allow)还可以定义为deny
Casbin 可以:
- 支持自定义请求的格式,默认的请求格式为{subject, object, action}。
- 具有访问控制模型model和策略policy两个核心概念。
- 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
- 支持内置的超级用户 例如:root 或 administrator。超级用户可以执行任何操作而无需显式的权限声明。
- 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*
Casbin 不能:
-
身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。
-
管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。
RBAC(Role-Based Access Control)即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。
二、快速开始
<-- https://mvnrepository.com/artisfact/org.casbin/jcasbin -->
<dependency>
<groupId>org.casbin</groupId>
<artifactId>jcasbin</artifactId>
<version>1.x.y</version>
</dependency>
SpringBoot版
<dependency>
<groupId>org.casbin</groupId>
<artifactId>casbin-spring-boot-starter</artifactId>
</dependency>
Casbin 拥有两个重要的文件
- model.conf:存储了访问模型
- policy.csv:存储了特定的用户权限配置
2.1 载入配置
- 使用时,如下方式载入
import org.casbin.jcasbin.main.Enforcer;
Enforcer e = new Enforcer("path/to/model.conf", "path/to/policy.csv");
- 通过yml文件载入
casbin:
#是否开启Casbin,默认开启
enableCasbin: true
#是否使用线程同步的Enforcer,默认false
useSyncedEnforcer: false
#是否开启策略自动保存,如适配器支持该功能,默认开启
autoSave: true
#存储类型[file,jdbc],目前支持的jdbc数据库[mysql(mariadb),h2,oracle,postgresql,db2]
#欢迎编写并提交您所使用的jdbc适配器,参见:org.casbin.adapter.OracleAdapter
#jdbc适配器将主动寻找您在spring.datasource配置的数据源信息
#默认使用jdbc,并使用内置h2数据库进行内存存储
storeType: jdbc
#当使用jdbc时,定制化数据库表名,默认表名是casbin_rule
tableName: casbin_rule
#数据源初始化策略[create(自动创建数据表,如已创建则不再进行初始化),never(始终不进行初始化)]
initializeSchema: create
#本地模型配置文件地址,约定默认读取位置:classpath:casbin/model.conf
model: classpath:casbin/model.conf
#如默认位置未找到模型配置文件,且casbin.model未正确设置,则使用内置默认rbac模型,默认生效
useDefaultModelIfModelNotSetting: true
#本地策略配置文件地址,约定默认读取位置:classpath:casbin/policy.csv
#如默认位置未找到配置文件,将会抛出异常
#该配置项仅在casbin.storeType设定为file时生效
policy: classpath:casbin/policy.csv
#是否开启CasbinWatcher机制,默认不开启
#如开启该机制,则casbin.storeType必须为jdbc,否则该配置无效
enableWatcher: false
#CasbinWatcher通知方式,默认使用Redis进行通知同步,暂时仅支持Redis
#开启Watcher后需手动添加spring-boot-starter-data-redis依赖
watcherType: redis
#异常抛出时机控制
exception:
... 详见附表-异常设定
policy因为比较多,一般存储在数据库中,我们这里先放在csv中作为简单的开始。
使用如下配置
casbin:
enableCasbin: true
storeType: file
model: classpath:casbin/model.conf
useDefaultModelIfModelNotSetting: true
policy: classpath:casbin/policy.csv
2.2 如何判断权限
我们通过对请求用enforce进行判断
String sub = "alice"; // 想要访问资源的用户
String obj = "data1"; // 将要被访问的资源
String act = "read"; // 用户对资源进行的操作
// 这个是默认格式,我们可以基于自己想要的在model.conf中修改
// 如:enforce(sub, obj, act,act2)
if (e.enforce(sub, obj, act) == true) {
// 允许alice读取data1
} else {
// 拒绝请求,抛出异常
}
2.3 model.conf
可以参考官网-支持的Models
2.3.1 基本格式
# 请求定义
# 它明确了 e.Enforce(...) 函数中参数的含义。
[request_definition]
r = sub, obj, act
# sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。
# 比如:user, home/detail, post
# 但是, 你可以自定义你自己的请求表单, 如果不需要指定特定资源,则可以这样定义 sub,act,或者如果有两个访问实体, 则为 sub,sub2,obj,act。
# 策略定义
# 是策略部分的定义,由它定义我们policy.csv中读出的权限格式
# 如下是两种策略 p 和 p2
[policy_definition]
p = sub, obj, act
p2 = sub, act
# 是策略效果的定义。
# 它确定如果多项政策规则与请求相符,是否应批准访问请求。
# p.eft 代表p的决策结果(是否匹配)
# 如下,代表存在一个匹配的策略规则就通过。
[policy_effect]
e = some(where (p.eft == allow))
# 相反
# 如下,代表存在一个不匹配就不通过。
[policy_effect]
e = !some(where (p.eft == deny))
# 请求和策略的匹配规则
# 它定义了如何根据请求评估策略规则
# 也就是 p.eft 的值
# 如下就是要求完全一致,才可以访问
# 除了与或非,也支持 in 判断是否再数组中,eval执行CSV中的判断语句
[matchers]
m = (r.sub == p.sub && r.obj == p.obj && r.act == p.act)
我们可以将r.sub.Age > 18这类语句放入eval中
CSV
p, r.sub.Age > 18, /data1, read
p, r.sub.Age < 60, /data2, write
2.3.2 SpringBoot下的使用
为了方便,我们这里引入一个依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
我们准备如下权限
policy.csv
p,user,post
p2,admin,/info,get
model.conf
[request_definition]
r = sub, act
r2 = sub, obj, act
[policy_definition]
p = sub, act
p2 = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
e2 = some(where (p.eft == allow))
[matchers]
m = (r.sub == p.sub && r.act == p.act)
m2 = (r2.sub == p2.sub && r2.obj == p2.obj && r2.act == p2.act)
注意,这里effect e2需要用p,而不是p2
yml
casbin:
enableCasbin: true
model: classpath:casbin/model.conf
useDefaultModelIfModelNotSetting: true
policy: classpath:casbin/policy.csv
Java
package cn.aos.aoscasbin.controller;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.casbin.jcasbin.main.Enforcer;
import org.casbin.jcasbin.util.EnforceContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
@Slf4j
@AllArgsConstructor
public class test {
private final Enforcer enforcer;
@GetMapping
public void test(){
log.info("{}",enforcer.enforce("user","post"));
log.info("{}",enforcer.enforce("admin","post"));
// 使用第二个匹配器
EnforceContext enforceContext = new EnforceContext("2");
log.info("{}",enforcer.enforce(enforceContext,"admin","/info","get"));
log.info("{}",enforcer.enforce(enforceContext,"user","/info","post"));
return;
}
}
另外,请注意,EnforceContext 生成后是统一指定第几个匹配,我们仍然可以通过提供的set方法,单独设置,指定想要e、p、r、m。
2.3.3 匹配的函数
内置函数
详情可自行查看-函数
下列函数都需要两个参数
- keyGet and keyGet2 将返回匹配通配符(*)的字符串,如果没有匹配返回 “”
- 其余函数将返回两个参数是否匹配
我们只需要直接使用即可(在conf中)。
自定义函数
public class CustomFunc extends CustomFunction {
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
String key1 = FunctionUtils.getStringValue(arg1, env);
String key2 = FunctionUtils.getStringValue(arg2, env);
if (key1.equals("/alice_data2/myid/using/res_id") && key2.equals("/alice_data/:resource")) {
return AviatorBoolean.valueOf(true);
} else if (key1.equals("/alice_data2/myid/using/res_id") && key2.equals("/alice_data2/:id/using/:resId")) {
return AviatorBoolean.valueOf(true);
} else {
return AviatorBoolean.valueOf(false);
}
}
@Override
public String getName() {
return "keyMatchCustom";
}
}
CustomFunc customFunc = new CustomFunc();
/**
* @param 注册函数名称
* @param 函数
* */
enforcer.addFunction(customFunc.getName(), customFunc);
注意,只有通过addFunction注册后,才能在对应的conf中直接使用
2.3.4 基于角色的访问控制
# 角色定义
# 是RBAC角色继承关系的定义,Casbin 支持 RBAC 系统的多个实例。
# 例如, 用户可以具有角色及其继承关系, 资源也可以具有角色及其继承关系。
# 这两个 RBAC 系统不会互相干扰。
[role_definition]
g = _, _
g2 = _, _
上述角色定义表明, g 是一个 RBAC系统, g2 是另一个 RBAC 系统。
_, _表示角色继承关系的前项和后项,即前项继承后项角色的权限。
我们在CSV中
g,用户,角色(代表用户为该角色)
g,角色1,角色2(代表角色1继承角色2)
一般来讲,如果需要进行角色和用户的绑定,直接使用g即可。
当需要表示角色(或者组)与用户和资源的绑定关系时,可以使用 g 和 g2 这样的表现形式。
在Casbin里,我们以policy表示中实际的用户角色映射关系 (或是资源-角色映射关系),例如:
这里时 alice 有 data2_admin的权限
p, data2_admin, data2, read
g, alice, data2_admin
我们通过 g()来判断对应的角色,接下来再用p去查询角色权限
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
角色用户资源绑定关系,可以如下写法
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
g2 = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
角色的层次
Casbin 的 RBAC 支持 RBAC1 的角色层次结构功能
如果 alice具有role1, role1具有role2,则 alice 也将拥有 role2 并继承其权限。因此, 此时role1的层次结构级别为2。
对于Casbin中的内置角色管理器, 可以指定最大层次结构级别。 默认值为10。 这意味着终端用户 alice 只能继承10个级别的角色。
区分用户和角色
在RBAC中,Casbin不对用户和角色进行区分。 它们都被视为字符串。
- 如果你只使用单层的RBAC模型(角色不会成为另一个角色的成员)。
可以使用 e.GetAllSubjects() 获取所有用户,e.GetAllRoles() 获取所有角色。
它们会为规则 g, u, r 分别列出所有的 u 和 r。
- 如果你在使用多层RBAC(带有角色继承),并且你的应用没有记录下一个名字(字符串)对应的是用户还是角色,或者你将用户和角色用相同的名字命名。
那么你可以可以给角色加上像 role::admin 的前缀再传递到Casbin中。
由此可以通过查看前缀来区分用户和角色。
隐式角色权限
当用户通过RBAC层次结构继承角色或权限,而不是直接在策略规则中分配它们时,我们将这种类型的分配称为 implicit。
也就是我们在代码中添加的,而不是在CSV中添加的。
比如(详情请自行查阅API)
boolean added = e.addGroupingPolicy("group1", "data2_admin");
// 或者
String[][] groupingRules = {
{"ham", "data4_admin"},
{"jack", "data5_admin"}
};
boolean areRulesAdded = e.addGroupingPolicies(groupingRules);
此外,带有name就是我们可以定义g,p(ptype)这种不同的数据
要查询这种隐式关系,需要使用以下两个api:
GetImplicitRolesForUser()以及 GetImplicitPermissionsForUser() 替代GetRolesForUser() 以及GetPermissionsForUser()
域内RBAC
我们可以将角色划分在不同域中(即不同应用、系统下)
[role_definition]
g = _, _, _
如此我们进一步划分了角色作用范围
p, admin, tenant1, data1, read
p, admin, tenant2, data2, read
g, alice, admin, tenant1
g, alice, user, tenant2
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
我们在g()中对比了g与r域(dom)的关系
如果可能,还需要在后面对比 p和r的域对应关系r.dom == p.dom
角色与函数
如下,与之前的很类似
ptype(g,p这些),绑定的名称,实现
// 自定义函数
enforcer.addNamedMatchingFunc("g", "", new BiPredicate<String, String>() {
@Override
public boolean test(String s, String s2) {
return false;
}
});
// 域匹配与自定义函数
enforcer.addNamedDomainMatchingFunc("g","", new BiPredicate<String, String>() {
@Override
public boolean test(String s, String s2) {
return false;
}
});
2.3.5 优先级模型
通过隐式优先级加载策略
顺序决定了策略的优先级,策略出现的越早优先级就越高,也就是使用了默认顺序
[policy_effect]
e = priority(p.eft) || deny
通过显式优先级加载策略
策略定义中的优先级令牌名称必须是“优先级”,较小的优先级值将具有较高的优先级。
如果优先级有非数字字符,它将是被排在最后,而不是导致报错。
现在,明确的优先级仅支持 添加策略 & 添加策略,如果 升级策略 被调用,那么不应该改变优先级属性。
[request_definition]
r = sub, obj, act
[policy_definition]
p = priority, sub, obj, act, eft
[role_definition]
g = _, _
[policy_effect]
e = priority(p.eft) || deny
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
p, 10, data1_deny_group, data1, read, deny
p, 10, data1_deny_group, data1, write, deny
p, 10, data2_allow_group, data2, read, allow
p, 10, data2_allow_group, data2, write, allow
p, 1, alice, data1, write, allow
p, 1, alice, data1, read, allow
p, 1, bob, data2, read, deny
g, bob, data2_allow_group
g, alice, data1_deny_group
基于角色和用户层次结构以优先级加载策略
角色和用户的继承结构只能是多棵树,而不是图。
如果一个用户有多个角色,您必须确保用户在不同树上有相同的等级。
如果两种角色具有相同的等级,那么出现早的策略(相应的角色)就显得更加优先。
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act, eft
[role_definition]
g = _, _
[policy_effect]
e = subjectPriority(p.eft) || deny
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
subject -> sub
p, root, data1, read, deny
p, admin, data1, read, deny
p, editor, data1, read, deny
p, subscriber, data1, read, deny
p, jane, data1, read, allow
p, alice, data1, read, allow
g, admin, root
g, editor, admin
g, subscriber, admin
g, jane, editor
g, alice, subscriber
2.3.6 超级管理员
[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 || r.sub == "root"
它说明了对于定义的 request_definition, policy_definition, policy_effect 和 matcher, Casbin 判断如果请求可以匹配的上策略,或者更重要的,如果 sub 是超级用户的话。
一旦判决正确,就允许授权,而且用户有权做一切。
就像Linux系统的root一样,用户被授权为 root, 我们就有访问所有文件和设置的权限。 因此,如果我们想要 sub 能够完全访问整个系统。 我们可以让它成为超级管理员,然后 sub 才有权做一切。
2.4 存储操作
2.4.1 Model存储
与 policy 不同,model 只能加载,不能保存。 因为作者认为 model 不是动态组件,不应该在运行时进行修改,所以没有实现一个 API 来将 model 保存到存储中。
因此只能从
- 从 .CONF 文件中加载 model
- 从代码加载 model
- 从字符串加载的 model
2.4.2 Policy存储
从 CSV 文件载入策略
这是我们最开始的方式。
数据库存储格式
要使用数据库存储,请参考最开始的yml配置
我们可以手动为其添加配置,也可以用enforce添加
-
id: 只存在于数据库中作为主键。 不作为 Casbin策略的一部分。它生成的方式取决于特定的适配器
-
ptype: 它对应 p, g, g2, 等等。
-
v0-v5: 列名称没有特定的含义, 并对应 策略csv 中的值。 列数取决于自定义的数量。 理论上,可以有无限的列数。 但通常在适配器中只有 6 列。 如果您觉得还不够,请向相应的适配器仓库提交问题。
适配器
适配器更换、指定此处省略。详情请翻看-适配器
在Casbin中,策略存储作为adapter(Casbin的中间件) 实现。
Casbin用户可以使用adapter从存储中加载策略规则 (aka LoadPolicy()) 或者将策略规则保存到其中 (aka SavePolicy())。
为了保持代码轻量级,作者没有把adapter代码放在主库中。
如果使用显式或隐式adapter调用casbin.NewEnforcer(),策略将自动加载。
可以调用e.LoadPolicy() 来从存储中重新加载策略规则。
如果adapter不支持Auto-Save特性,则在添加或删除策略时不能将策略规则自动保存回存储器。 你必须手动调用 SavePolicy() 来保存所有的策略规则
顺便说一下,我们可以看到,我们引入的Spring Boot的Casbin依赖,用的是JDBC Adapter
政策子集加载
一些adapter支持过滤策略管理。 这意味着Casbin加载的策略是基于给定过滤器的存储策略的子集。 当解析整个策略成为性能瓶颈时,这将会允许在大型多租户环境中有效地执行策略。
要使用支持的adapter处理过滤后的策略,只需调用 LoadFilteredPolicy 方法。 过滤器参数的有效格式取决于所用的适配器。 为了防止意外数据丢失,当策略已经加载, SavePolicy 方法会被禁用。
(也就是担心,过滤的数据覆盖了原数据)
我们如果要过滤,可以如下写法
enforcer.loadFilteredPolicy(new FilteredAdapter.Filter());
当然这是一个空的过滤器,filter类如下,我们需要设置过滤策略
public static class Filter {
public String[] p;
public String[] g;
public Filter() {
}
}
请注意,笔者是在数据库模式下使用。非数据库模式下(至少笔者使用的这一依赖)不可以用。
可以看到,成功过滤
FilteredAdapter.Filter filter = new FilteredAdapter.Filter();
// 此处过滤掉不是 p 数据的
filter.p = new String[]{"p","",""};
log.info("{}",enforcer.getAllSubjects());
enforcer.loadFilteredPolicy(filter);
log.info("{}",enforcer.getAllSubjects());
三、扩充功能部分
略
请自行参看文档扩充功能部分
四、API部分
- 含有name的,一般可用于指定ptype(也就是p,g)
- 含有Ex的的,一般可用于解释执行内容
- 含有Batch的,一般可接收数组参数,批量执行
五、其他
多数据源
@Configuration
public class CasbinDataSourceConfiguration {
@Bean
@CasbinDataSource
public DataSource casbinDataSource() {
return DataSourceBuilder.create().url("jdbc:h2:mem:casbin").build();
}
}
异常设定
附录
[1]官方文档
[2]官方实例-一般版本
[3]官方实例-SpringBoot版本