This answer 已经触及到 OP 真正要问的问题的核心。我将通过使用 hasPermission 表达式稍微深入了解幕后发生的事情来补充这个答案。
回顾
让我们先回顾一下this answer。回答者检测到 OP 确实打算使用带有两个参数的注释:
@PreAuthorize("hasPermission(#opetussuunnitelmaDto, 'LUONTI')")
之所以出现混淆,是因为 OP 在代码中看到了一个方法 hasPermission,它接受了三个参数,但不知道为第一个参数传递什么。回答者确认Spring框架本身提供了第一个参数,即Authentication对象,所以在注解中我们只需要传递两个参数。
深入探索
为了更详细地了解发生了什么,让我们分析一下hasPermission 在 Spring OOTB 中的工作原理。我不会详细介绍每一个细节,但会勾勒出正在发生的事情的主要流程。希望这不仅能阐明哪个重载方法与hasPermission SpEL 表达式相关联,正如 OP 所要求的那样,而且还将揭示整个 ACL 框架如何在底层解释 hasPermission 表达式;这将使我们对hasPermission 表达式的含义以及如何解释和使用它更有信心。
让我们从头开始。
关于授权前/授权后的小提示
要了解hasPermission 表达式,我们确实需要了解授权前/授权后。但是,由于 OP 没有询问这一点,因此假定它是已知的,并且我不会通过 @PreAuthorize 和 @PostAuthorize 注释详细介绍方法保护。读者请参考here 了解更多信息。在这里只要说我们假设hasPermission 表达式嵌入在这样的注释中以保护方法或返回对象就足够了。 hasPermission 表达式反过来将评估为真或假。如果它评估为真,则 Spring 框架将在预授权的情况下允许方法调用继续进行,或者在后授权的情况下允许返回对象。否则,它将阻止访问。这些注释就足够了。我们真正想知道的是 Spring 如何解释 hasPermission 表达式本身,以得出真/假值。
权限评估类
因此,hasPermission 将评估为真或假。但是怎么做?好吧,正如 OP 所提到的,Spring 将权限评估委托给嵌套在 MethodSecurityExpressionHandler Bean 中的 PermissionEvaluator 对象。如果您已经设置了 Spring ACL,那么您很可能已经将 AclPermissionEvaluator 注册为 Spring 使用的权限评估器。例如,如果你用代码配置了 Spring ACL,你可能会有这样的东西:
@Bean
public MethodSecurityExpressionHandler
defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler
= new DefaultMethodSecurityExpressionHandler();
AclPermissionEvaluator permissionEvaluator
= new AclPermissionEvaluator(aclService());
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
如果您没有这样做,则默认权限评估器将是 DenyAllPermissionEvaluator,我相信您已经猜到在所有情况下都会拒绝权限:肯定是安全的默认值。
从注解到方法
因此,如上所述,将 AclPermissionEvaluator 类插入 Spring 安全框架中,Spring 表达式语言 (SpEL) 中的所有 hasPermission 表达式都将委托给 AclPermissionEvaluator 进行评估。我还没有研究 SpEL 表达式最终如何导致调用 AclPermissionEvaluator 中的方法的确切细节,但我认为不需要这些知识来解释 hasPermission 表达式的含义。 IMO,在这个级别上,所有需要知道的是哪个注释会导致哪个方法调用。 this answer 已经涵盖了这一点。但让我在这里回顾一下。首先,我们注意到hasPermission 方法在AclPermissionEvaluator 中以及实际上在PermissionEvaluator 的任何实现中都被重载。其中一种方法采用 3 个参数,另一种采用 4 个参数:
//3-Arg-Method
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
//4-Arg-Method
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission);
另一方面,hasPermission 表达式也有两个用例。其中一个传入 2 个参数,另一个传入 3 个参数。这些已经在this answer 中指出。但是我们在这里将它们标记为表达式,而不是方法,以免混淆两者:
hasPermission('#targetDomainObject', 'permission') //2-arg-expression
hasPermission('targetId', 'targetType', 'permission') //3-arg-expression
我们现在可以将两者联系起来:
- 如果使用
//2-arg-expression,则调用//3-Arg-Method。
- 如果使用
//3-arg-expression,则调用//4-Arg-Method。
方法从哪里获得额外的参数?同样,here 已经回答了这个问题,但回顾一下,Spring 安全框架基于安全上下文提供的额外参数是两种情况下的第一个参数,即名为 authentication 的 Authentication 参数。我还没有研究过 Spring 框架是如何做到这一点的,但对我来说,只要知道 Spring 安全性可以在这个上下文中获取一个身份验证对象就足够了。
好的,但是其他参数呢?接下来让我们看看这个。为了避免这个答案变得太大,我将只关注使用//2-arg-expression 并调用//3-Arg-Method 的情况。
hasPermission 方法的参数
如前所述,让我们只关注这个方法:
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
如前所述,第一个参数 authentication 对象是通过 Spring 安全性推断的。我还没有仔细研究过这种情况是如何发生的,但我相信为了这篇文章的目的,我们需要知道的只是了解身份验证对象包含:
- 用户,即委托人,例如“爱丽丝”
- 所有角色,即已授予该用户的权限,例如“管理员”或“编辑”
在 Spring ACL 中,我们使用通用术语 SID 来指代诸如“Alice”之类的主体,或诸如“编辑者”之类的权威。因此,authentication 对象不仅包含一个 SID,还包含它们的完整列表。这个列表的顺序很重要,我们稍后会看到。
hasPermission 方法的其余参数通过hasPermission 表达式传递。这些都输入为Object。同样,为了使这篇文章更短,我将只关注一个用例。事实上,让我们关注 OP 提到的原始用例的略微修改版本:
@PreAuthorize("hasPermission(#opetussuunnitelmaDto, 'READ')")
OpetussuunnitelmaDto addOpetussuunnitelma(OpetussuunnitelmaDto opetussuunnitelmaDto);
- 在子表达式
#opetussuunnitelmaDto 中使用# 符号是在SpEL 中指定方法addOpetussuunnitelma 的opetussuunnitelmaDto 参数作为hasPermission 方法的targetDomainObject 传递的一种方式.
-
'READ' 参数更简单:它只是作为String 直接传递给hasPermission 方法的permission 参数。
从参数中提取有用信息
所以,我们现在知道了所有参数是如何提供给这个方法的:
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
但是Object 类型的参数从来没有多大用处。 Spring ACL 需要将这些参数转换为信息,以便从数据库中访问相关的 ACL 信息并进行权限检查。它通过委托给checkPermission 方法来做到这一点,该方法提取信息如下:
- 从身份验证对象中获取 SID 的有序列表。例如,假设用户“Alice”已登录并且她具有“admin”和“editor”权限。然后这个列表将包含“Alice”、“admin”和“editor”的 SID。存储该列表的变量是
List<Sid> sids。现在,这个列表的顺序很重要。让我们考虑一下为什么。假设您混合使用授权和拒绝 ACE。例如,我们可以向所有编辑者授予对某个对象的访问权。但是我们可能会拒绝用户 Jane。如果作为编辑的 Jane 尝试访问该对象,我们是基于她是 Jane 拒绝访问,还是基于她是编辑而授予访问权限?因此,SID 列表的顺序很重要。第一个匹配的获胜。那么是什么控制了返回 SID 的顺序呢?嗯,这个责任在于SidRetrievalStrategy,默认情况下是SidRetrievalStrategyImpl。通过查看这个类的getSids 方法,我们看到主 SID,即 Alice,在列表中被赋予了首要位置。此后遵循授予的权限。我没有深入研究当局本身是如何排序的细节,但在我看来它只是插入顺序,除了角色层次结构在起作用的情况,在这种情况下,顺序可能遵循层次结构。对我来说,Alice 将被授予列表中的第一个位置是有道理的。如果 Alice 自己被授予/拒绝访问任何东西,那么直觉上认为这会覆盖基于她所拥有的角色而被授予的任何东西。例如,如果我们想要拒绝对 Alice 的访问,即使她是编辑,那么特定的拒绝应该优先。另一方面,我们可能希望禁止所有编辑者访问一个对象,但为 Alice 设置一个例外。同样,将 Alice 放在列表的首位可确保执行此逻辑。
- 权限对象,到目前为止只是一个
Object,通过resolvePermission 方法解析为Permission 对象列表。存储它的变量是List<Permission> requiredPermission。现在,回想一下,我们关注的是这个权限是单个字符串的情况,即"READ"。在这种情况下,如果 Spring 保留其默认行为,权限解析器将使用反射检查此 String 与类 BasePermission 中的所有静态常量,并将返回匹配的常量。实际进行最终转换的代码是类DefaultPermissionFactory 中的方法buildFromMask。如果没有找到名称与"READ" 匹配的BasePermission 成员,则代码将抛出异常。实际上,在 OP 的用例中,给定的权限是 "LUONTI",它不会匹配 BasePermission 中的任何内容 - 在这种情况下,开发人员需要覆盖 BasePermission 或创建自己的权限类。但我们不会在这里讨论。我们还注意到,一般来说,表达式可能会产生一个权限列表,但在我们的特定情况下,我们只为传递给 SpEL 表达式的一个字符串获得一个权限。
- ACL 本身是根据对象检索的。实际上,在
hasPermission 方法中,域对象被转换为对象 ID,checkPermission 然后使用该对象 ID 通过 ACL 服务在数据库中查询该 ACL:Acl acl = this.aclService.readAclById(oid, sids);。
Spring 现在拥有了进行“是/否”检查所需的所有信息:当前登录的用户是否有权访问该对象?它通过委托给PermissionGrantingStrategy Bean 上的isGranted 方法来实现。默认情况下,这是通过DefaultPermissionGrantingStrategy 实现的。
isGranted ...我们快到了
当我们查看此方法时,很明显顺序对于 ACL 中的 ACE 列表和 SID 列表确实很重要。顺序对于权限列表也有些重要,但不太重要 - 它只确定哪个权限被解释为拒绝访问的“第一个”权限,如果 (public*) isGranted 表达式的结果评估为 false;据我所知,这仅用于记录/调试目的,以便管理员可以尝试修复最有可能首先被破坏的权限。
对于 ACE 和 SID,顺序确实很重要,因为与 SID 匹配的第一个 ACE 具有优先权,并且不会针对该权限执行其他匹配。如果匹配结果为允许,则整个 isGranted 函数返回 true。否则,如果该权限不匹配或存在拒绝,则代码将转到下一个权限并尝试该权限。这样,我们可以看到权限列表是用 OR 类型的逻辑检查的:只需授予其中一个权限,isGranted 就可以成功。
检查给定 ACE 是否与给定权限和 SID 匹配的实际逻辑呢?好吧,SID 位很简单:只需从 ACE 中取出 SID 字段并比较:ace.getSid().equals(sid)。如果 SID 匹配,则调用重载的 isGranted 函数,它只是比较掩码:
protected boolean isGranted(AccessControlEntry ace, Permission p) {
return ace.getPermission().getMask() == p.getMask();
}
IMO,这个方法确实应该被称为 isMatching,因为它应该为允许(即授予)和拒绝类型的权限返回 true。它只是一个匹配函数——允许/拒绝行为存储在ace.isGranting() 字段中。此外,函数名称isGranted 被重载*,更令人困惑。
还有some confusion 围绕为什么它不使用按位逻辑,但不用担心,如果您愿意,您可以轻松地覆盖该方法,如链接问题的答案中所述。
结论
回顾一下,OP最初问:
spring security中的hasPermission如何解读?
此答案深入探讨了hasPermission 的机制,以了解如何解释它。总结:
-
hasPermission SpEL 表达式链接到 Spring ACL 中 AclPermissionEvaluator 中的重载 hasPermission 方法之一,其中 Authentication 对象由 Spring 安全性自动填充。
-
hasPermission SpEL 表达式的参数通过 Spring ACL 机制向下传递。
- Spring ACL 检查三个列表:SID、权限、ACE(ACL 本身),对于其中两个列表,顺序很重要,以确定“用户是否有权访问此对象? "
- 每个权限只执行一次 ACE 匹配,匹配基于 SID 和重载的
isGranted 函数,可以覆盖例如如果开发者想使用按位逻辑。
脚注
*isGranted 函数有两个版本。公开的确实会检查列表中的某些权限是否授予某些 SID。而受保护的确实应该被称为 isMatching 之类的东西,因为它会检查匹配的 ACE。