这个答案已经触及了 OP 真正要求的核心。我将通过更深入地研究hasPermission
表达式背后发生的事情来补充这个答案。
回顾
让我们先回顾一下这个答案。回答者检测到 OP 确实打算使用带有两个参数的注释:
@PreAuthorize("hasPermission(#opetussuunnitelmaDto, 'LUONTI')")
之所以产生混淆,是因为 OPhasPermission
在代码中看到了一个采用三个参数的方法,并且无法弄清楚要为第一个参数传递什么。回答者确认 Spring 框架本身提供了第一个参数,即Authentication
对象,因此在注解中我们只需要传递两个参数。
更深的潜水
为了更详细地了解发生了什么,让我们分析一下hasPermission
Spring OOTB 中的工作原理。我不会详细介绍每一个细节,但会勾勒出正在发生的事情的主要流程。希望这不仅会阐明哪个重载方法与hasPermission
SpEL 表达式相关联,正如 OP 所要求的那样,而且还将揭示整个 ACL 框架如何在幕后解释hasPermission
表达式;这将使我们对hasPermission
表达式的含义以及如何解释和使用它有更大的信心。
所以让我们从顶部开始。
关于前/后授权的小说明
要理解这个hasPermission
表达式,我们真的需要理解 pre/post 授权。但是,由于 OP 没有询问这一点,因此假定它是已知的,并且我不会通过@PreAuthorize
and@PostAuthorize
注释详细介绍方法保护。读者可以参考这里以获取更多信息。在这里只要说我们将假设hasPermission
表达式嵌入在这样的注释中以保护方法或返回对象就足够了。这hasPermission
反过来,表达式将评估为真或假。如果它评估为真,则 Spring 框架将在预授权的情况下允许方法调用继续进行,或者在后授权的情况下允许返回对象。否则,它将阻止访问。这些注释就足够了。我们真正想知道的是 Spring 如何解释hasPermission
表达式本身,以得出真/假值。
权限评估器类
因此,hasPermission
将评估为真或假。但是怎么做?好吧,正如 OP 所提到的,Spring 将权限评估委托给PermissionEvaluator
嵌套在MethodSecurityExpressionHandler
Bean 内的对象。如果你已经设置了 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 安全框架hasPermission
中,Spring 表达式语言 (SpEL) 中的所有表达式都将委托给AclPermissionEvaluator
评估。我还没有研究 SpEL 表达式最终如何导致调用内部方法的确切细节AclPermissionEvaluator
,但我认为不需要这些知识来解释hasPermission
表达式的含义。IMO,在这个级别上,所有需要知道的是哪个注释会导致哪个方法调用。这个答案已经涵盖了这一点。但让我在这里回顾一下。首先,我们注意到该方法在和实际上在任何实现中都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 个参数。这些已在此答案中指出。但是让我们在这里将它们标记为表达式,而不是方法,以免混淆两者:
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
调用 。
这些方法从哪里获得额外的参数?同样,这里已经回答了这个问题,但回顾一下,Spring 安全框架提供的基于安全上下文的额外参数是两种情况下的第一个参数,即名为 的Authentication
参数authentication
。我还没有研究过 Spring 框架是如何做到这一点的,但对我来说,只要知道 Spring 安全性可以在这个上下文中获取一个身份验证对象就足够了。
好的,但是其他论点呢?接下来让我们看看这个。为了避免这个答案变得太大,我将只关注//2-arg-expression
使用和//3-Arg-Method
调用的情况。
hasPermission
方法的参数
如前所述,让我们只关注这个方法:
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
如前所述,第一个参数,authentication
对象是通过 Spring 安全性推断的。我还没有仔细研究过这种情况是如何发生的,但我相信为了这篇文章的目的,我们需要知道的只是了解身份验证对象包含:
- 用户,即主体,例如“Alice”
- 所有角色,即已授予该用户的权限,例如“管理员”或“编辑”
在 Spring ACL 中,我们使用通用术语 SID 来指代诸如“Alice”之类的主体,或诸如“编辑者”之类的权威。因此,该authentication
对象不仅包含一个 SID,还包含它们的完整列表。这个列表的顺序很重要,我们稍后会看到。
该hasPermission
方法的其余参数通过hasPermission
表达式传递。这些都键入为Object
. 同样,为了使这篇文章更短,我将只关注一个用例。实际上,让我们关注 OP 提到的原始用例的略微修改版本:
@PreAuthorize("hasPermission(#opetussuunnitelmaDto, 'READ')")
OpetussuunnitelmaDto addOpetussuunnitelma(OpetussuunnitelmaDto opetussuunnitelmaDto);
- 在子表达式中使用 # 符号
#opetussuunnitelmaDto
是在 SpEL 中指定方法的opetussuunnitelmaDto
参数作为方法的参数addOpetussuunnitelma
传入targetDomainObject
的hasPermission
一种方式。
'READ'
参数更简单:它只是作为String
直接传递给方法的permission
参数hasPermission
。
从参数中提取有用信息
所以,我们现在知道所有参数是如何提供给这个方法的:
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
,通过 方法解析为Permission
对象列表resolvePermission
。存储这个的变量是List<Permission> requiredPermission
. 现在,回想一下,我们关注的是这个权限是单个字符串的情况,即"READ"
. 在这种情况下,如果 Spring 保留其默认行为,权限解析器将使用反射来检查String
类中的所有静态常量BasePermission
,并返回匹配的常量。实际进行最终转换的代码是buildFromMask
类中的方法DefaultPermissionFactory
。如果没有BasePermission
找到名称匹配的成员"READ"
,则代码将抛出异常。实际上,在 OP 的用例中,给予的许可是"LUONTI"
,它不会匹配任何东西BasePermission
- 在这种情况下,开发人员需要覆盖BasePermission
或创建他们自己的权限类。但我们不会在这里讨论。我们还注意到,一般来说,表达式可能会产生一个权限列表,但在我们的特定情况下,我们只为传递给 SpEL 表达式的一个字符串获得一个权限。
- ACL 本身是根据对象检索的。实际上,在该
hasPermission
方法中,域对象被转换为对象 ID,checkPermission
然后用于通过 ACL 服务在数据库中查询该 ACL:Acl acl = this.aclService.readAclById(oid, sids);
。
Spring 现在拥有执行“是/否”检查所需的所有信息:当前登录的用户是否有权访问该对象?它通过委托给BeanisGranted
上的方法来实现。PermissionGrantingStrategy
默认情况下,这是通过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
被重载*,更令人困惑。
关于为什么不使用按位逻辑也有一些混淆,但不用担心,如果您愿意,您可以轻松地覆盖该方法,如链接问题的答案中所述。
结论
回顾一下,OP最初问:
spring security中的hasPermission如何解读?
这个答案深入hasPermission
探讨了如何解释它的机制。总之:
hasPermission
SpEL 表达式链接到Spring ACL 中的重载方法之一,该对象hasPermission
由Spring 安全性自动填充。AclPermissionEvaluator
Authentication
hasPermission
SpEL 表达式的参数通过 Spring ACL 机制向下传递。
- Spring ACL 检查三个列表:SID、权限、ACE(ACL 本身),对于其中两个列表,顺序很重要,以确定对“用户是否有权访问此对象?”问题的最终 YES/NO 答案。
- 每个权限只执行一次 ACE 匹配,并且匹配基于 SID 和重载
isGranted
函数,例如,如果开发人员想要使用按位逻辑,则可以覆盖该重载函数。
脚注
*该功能有两个版本isGranted
。公共确实会检查列表中的某些权限是否已授予某些 SID。而受保护的确实应该被称为类似的东西,isMatching
因为它检查匹配的 ACE。