ABAC——基于属性的访问控制

ABAC 是一种访问控制模型,也被称为基于属性或者策略的访问控制。

其主要思想是通过评估与主体、对象、请求的操作以及在某些情况下的环境属性关联的属性来确定主体执行一组操作的授权。

ABAC以抽象程度更高的角度来描述权限,使得在复杂情况下比传统权限架构更具有灵活性,可维护性和可拓展性,配合优秀的架构设计可以使你的业务代码变得更加干净整洁。通过定义策略规则,ABAC可以表示可以评估许多不同属性的复杂规则集,稍加修改就可以将RBAC囊括在内。

为什么ABAC具有如此强大的能力呢?接下来让我们从一个案例引入:

案例

某天,项目组突然接到了一个紧急项目,需要为一家企业开发一个大型文件存储系统。其中你需要负责设计一个高性能、可靠且易于扩展的权限控制子系统,你的同事将负责未来几年的系统维护。

你立即按照RBAC的设计顺序和思想开始了设计工作

  1. 确定系统中的角色,根据企业的组织结构、职能和职位级别来定义角色。这个过程需要充分考虑系统的扩展性,确保角色定义能够适应组织的变化和新的业务需求。
  2. 确定了系统中的各项操作和功能,为每个操作和功能定义相应的权限。
  3. 并将各项权限与角色进行关联。

到此一切顺利,这是一个相对简单的任务,你非常出色的完成了这次设计。直到在第一次交接时,甲方和你的同事为了实现某些新功能对权限系统提出了需求:

  1. 动态管理接口
  2. 并且非管理员在非上班时间不允许上传和下载文件
  3. 所有员工上传文件需要某些特殊限制,比如ip在公司内或者携带可被验证的特殊签名

你迅速响应并完成了这个需求的开发工作。为了实现更细致的权限划分,你不得不将某些特殊的接口分离出来,这导致接口数量急剧增加。比如将各级角色上传下载资源的接口分离,为其分别添加在上班时间,ip在公司内和验证的特殊签名的判断。

此时,整个项目中充斥着权限相关的硬编码,几乎每个接口都有特定的权限条件需要满足。为了让接手的同事和前端同学能够理解这些特殊条件,你不得不将它们写入接口文档,这让他们感到困惑和烦恼。

使用ABAC进行细致的控制

如果你使用ABAC设计刚才的文件系统,就可以根据具体业务定义属性,如用户的部门、职位级别,文件的敏感性级别等。然后,通过定义策略规则,如只有部门经理级别及以上的用户才能访问敏感性级别为高的文件,从而自动化地决定访问权限,而无需为每个用户分配权限。

你可以更加轻松的拓展并完成刚才的新需求,例如将用户访问ip,时间与特殊签名加入属性,并通过修改统一的策略规则加强对属性进行判断。

此时鉴权设计与系统解耦变得更加容易,系统只需要装配所有与业务相关的属性并交由ABAC管理,所有和权限有关的约定都被集成到策略规则中。


ABAC概念中最重要的几个概念分别为属性,策略规则与基于关注点分离的推荐构成。接下来我将对几点依次进行说明。

属性

属性是ABAC中重要的几个概念之一,被来描述具体的操作,属性可以是关于任何事物和任何人的,通常来自对象、资源、操作和环境信息。一般来说,属性可以分为四类:

主题属性,描述尝试访问的用户的属性,例如年龄、权限、部门、角色、职位

操作属性,描述正在尝试的操作的属性,例如读取、删除、查看、批准

对象属性,描述被访问对象(或资源)的属性,例如对象类型(医疗记录、银行账户)、部门、分类或敏感性、位置

上下文属性,处理访问控制场景的时间、位置或某些特殊或者动态的属性

策略规则

策略规则是将属性集合在一起的语句,用来描述所有允许通过和不允许通过的属性集合。这样的操作既可以动态存储,也可以静态放置在文件内。例如:

  1. 可以存储在用户关联的信息中,对每个用户分别定义策略规则
  2. 可以动态或静态存储在角色关联的信息中,管理RBAC权限
  3. 可以动态或静态存储全局策略规则,提供全局统一的鉴权逻辑

在一些复杂案例中,可以通过分层允许底层补充或重写某些策略来达到更好的复用性和灵活度。

通过策略规则描述对资源的限制

用来描述其中描述的过程也可以被成为策略:某角色某操作(携带某些信息)(在某个时间段)是被允许的。

这样将对角色的判断与对访问的特定资源联合起来,使两者共同对鉴权结果产生贡献。例如:

  1. 用户可以查看与自己属于同一部门的文档
  2. 如果用户是文档的所有者并且文档处于草稿模式,则用户可以编辑文档
  3. 上午9点前禁止访问

如此我们可以收集资源并构建属性,最后对属性判断是否通过,若其中有一组方法得到满足,则判断操作通过。

推荐构成

ABAC按照关注点分离可以分为三个部分:即实施,判断与信息获取。

策略实施点(PEP),负责保护应用ABAC的应用程序和数据。PEP检查请求并生成授权请求,然后将授权请求发送给PDP。

策略决策点(PDP),是体系结构的大脑。这是根据已配置的策略评估传入请求的部分。PDP返回允许/拒绝决定。PDP也可以使用pip来检索丢失的元数据。

策略信息点(PIP),将PDP连接到外部属性源,例如LDAP或数据库。

如何决策

通过以上架构,我们将ABAC构成中的关注点分离开。假设我们现在已经可以收集到策略规则和访问时获取的属性,这时我们将如何进行适当的决策,将两者进行匹配,检查属性是否满足策略规则呢?

  1. 首先按照 strategy 将策略规则分为通过策略和拒绝策略,其中通过策略和拒绝策略内部的判断方式是一致的,应避免出现歧义。
  2. 判断属性是否满足单条策略时,判断作用对象是否匹配,若通过则继续判断所有 context 是否满足条件,判断时依据属性内其他信息。
  3. 统计判断结果,拒绝策略的优先级高于通过策略,且若不满足任意一条拒绝策略,则认为不满足 Policies 。判断属性满足策略规则条件为:满足至少一条通过策略且不满足所有拒绝策略。

使用Java进行简单实践

我们可以根据以上提到的细节完成ABAC的抽象定义,首先定义属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Attribute {
<T extends Option> T option();
<T extends Target> T target();
<T extends Visitor> T visitor();
AttributeContext context();
}
public interface Option {
}
public interface Target {
}
public interface Visitor {
}
public interface Context {
}
public record AttributeContext(Map<String, Object> values) implements Context {
}

我们创建的Attribute类内部包含:

  1. option,即操作属性,描述正在尝试的操作的属性,例如读取、删除、查看、批准
  2. target,描述被访问对象(或资源)的属性,例如对象类型(医疗记录、银行账户)、部门、分类或敏感性、位置
  3. visitor,即主题属性,描述尝试访问的用户的属性,例如年龄、权限、部门、角色、职位
  4. context,上下文(环境)属性,处理访问控制场景的时间、位置或动态方面的属性

使用自定义的record继承Attitude,或者直接使用类型实现接口,则可以被系统承认是一个Attribute。以同样的方法定义Policy:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Policy {
<T extends Visitor> List<T> visitors();
<T extends Option> List<T> options();
<T extends Target> List<T> targets();
Context context();
Strategy strategy();
}
public enum Strategy {reject, approval}
public record PolicyContext(Map<String, Map<String, Rule<Object>>> rules) implements Context {
public record DTO(Map<String, Map<String, Object>> rules) implements Context {
}
}

接下来建立策略设施的基本行为:

策略决策点,需要根据策略和属性信息判断某一次操作是否可以被接收。

1
2
3
4
5
6
7
8
9
10
public interface Decision<P extends Policy, A extends Attribute> {
default boolean decide(List<P> policies, A attribute) {
var approvals = policies.stream().filter(defaultPolicy -> defaultPolicy.strategy().equals(Strategy.approval));
var rejects = policies.stream().filter(defaultPolicy -> defaultPolicy.strategy().equals(Strategy.reject));
return approvals.anyMatch(policy -> approval(policy, attribute))
&& rejects.noneMatch(policy -> reject(policy, attribute));
}
boolean approval(P policies, A attribute);
boolean reject(P policies, A attribute);
}

策略实施点,实施策略并返回结果。

1
2
3
public interface Enforcement {
Result<Object> enforce(Attribute attribute);
}

策略信息收集点,收集策略信息,并负责策略信息的刷新。

1
2
3
4
5
public interface Information<V extends Visitor, O extends Option, T extends Target, P extends Policy> {
List<P> policies();
Information<V, O, T, P> refresh();
PolicySource getPolicySource();
}

为Information提供从数据源加载数据的抽象,并定义将policies序列化和反序列化的能力。

1
2
3
4
5
6
7
8
9
10
11
public interface PolicySource<T extends Policy, P extends PolicySource<T, P>> {
P registerRuleCreators(List<RuleCreator<Object, Object>> ruleCreators);
P registerRuleCreator(RuleCreator<Object, Object> ruleCreator);
P setStrSource(Supplier<String> strSource);
P setPolicySerialize(PolicySerialize<T> policySerialize);
List<T> load();
}
public interface PolicySerialize<T extends Policy> {
String serialize(List<T> policies, List<RuleCreator<Object, Object>> creators);
List<T> deserialize(String serialized, List<RuleCreator<Object, Object>> creators);
}

policies无法全部直接变成可以运行判断的程序,因此需要做一次转换,将context部分转换成可以运行的rules实体。

rule根据creator进行创建,同时提供了后期拓展新的rule的可能性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface Rule<T> {
boolean judge(T data);
String name();
Object param();
class Builder<T> {
private String name;
private Object param;
private BiPredicate<T, Object> rule;
public Builder<T> withName(String name) {
this.name = name;
return this;
}
public Builder<T> withParam(Object param) {
this.param = param;
return this;
}
public Builder<T> rule(BiPredicate<T, Object> rule) {
this.rule = rule;
return this;
}
public Rule<T> build() {
return CustomRule.of(this.param, this.name, this.rule);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class CustomRule<T> implements Rule<T> {
private final Object param;
private final String name;
private final BiPredicate<T, Object> rule;
private CustomRule(Object param, String name, BiPredicate<T, Object> rule) {
this.param = param;
this.name = name;
this.rule = rule;
}
static <T> CustomRule<T> of(Object param, String name, BiPredicate<T, Object> rule) {
return new CustomRule<>(param, name, rule);
}
public boolean judge(T data) {
return this.rule.test(data, this.param);
}
@Override public String name() {
return this.name;
}
@Override public Object param() {
return this.param;
}
}
1
2
3
4
public interface RuleCreator<P, T> {
Rule<T> create(String name, P param);
String ruleType();
}

将上述所有能力整合到接口

1
2
3
4
public interface ABACFactory<C extends ABACConfiguration> {
C getConfiguration();
ABAC create();
}
1
2
3
4
5
6
7
8
9
10
11
12
public interface ABACConfiguration<V extends Visitor, O extends Option, T extends Target, P extends Policy, A extends Attribute, t> {
ABACConfiguration<V, O, T, P, A, t> registerInformation(Information<V, O, T, P> information);
ABACConfiguration<V, O, T, P, A, t> registerDecision(Decision<P, A> decision);
ABACConfiguration<V, O, T, P, A, t> forClass(Class<? extends ABAC> clazz);
ABACConfiguration<V, O, T, P, A, t> registerRuleCreators(List<RuleCreator<Object, Object>> ruleCreators);
ABACConfiguration<V, O, T, P, A, t> registerRuleCreator(RuleCreator<Object, Object> ruleCreator);
ABACConfiguration<V, O, T, P, A, t> setStrSource(Supplier<String> strSource);
ABACConfiguration<V, O, T, P, A, t> setPolicySerialize(PolicySerialize<P> policySerialize);
Information<V, O, T, P> getInformation();
Decision<P, A> getDecision();
Class<? extends ABAC> getABACClass();
}
1
2
3
public interface ABAC {
Result<Object> enforce(Attribute attribute);
}

使用抽象工厂用配置类来约束ABAC的定义,产生的ABAC对象可以定义为策略实施点。以上是抽象的定义,用户选择使用提供的默认选项或者自定义ABAC的实现。

我们在仓库中提供了默认实现供大家使用,目前可以通过内部服务的方式部署使用。

目前可以很方便的实现以下效果的策略验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
"visitors": [{"id":1}],
"context":{
"rules":{
"time": {"before": "12:30"}
}
},
"strategy": "reject"
},
{
"context":{
"rules":{
"permission": {"containsAll": ["p1","p3"]},
"time": {"after": "11:30"}
}
},
"strategy": "approval"
}
]

以上策略代表的含义为对id为1的用户在12:30前进行拦截,对其他拥有p1和p3权限的用户在11:30后放行。

以上就是我使用Java对ABAC的简单实践,非常感谢大家阅读,作者致力于使文章适合所有程序员阅读(包括初学者),如有建议可以通过各种方式联系作者,非常感谢大家的支持。