问题描述
Spring Security 提供了诸如注解 (@PreAuthorize
) 之类的授权方法和特定于安全性的 SpEL 表达式,例如 hasRole
。原则上,这种表达语言可以扩展为支持 ABAC,但这并不能真正解决架构问题在哪里做。
应用程序架构的灵感来自 Clean Architecture(反过来又受到 Hexagonal Architecture 和 Onion Architecture 的启发)。基本构建块如下所示:
- Adapter:连接外部世界,例如。 G。 HTTP 端点
- 应用服务:与 DTO 相互转换、提供交易、执行授权
- 域:描述业务数据、规则和行为
在我看来,应用层是唯一适合执行授权的层。它不适合适配器层,因为单个应用程序服务可能有多个适配器,因此必须在多个地方重复授权。它显然不适合领域,因为它是一个技术方面,不应该成为领域逻辑的一部分。
典型的应用程序服务方法从客户端获取或多或少的“原始”参数,对其进行转换并将执行委托给域:
@Transactional
class Myapplicationservice {
fun cancelOrder(id: Long) {
val orderId = OrderId(orderId)
val order = orderRepository.findById(orderId)
order.cancel()
}
}
为了能够执行基于属性的访问控制(ABAC),必须先加载订单对象,以便首先提取属性。这也意味着 @PreAuthorize
函数上方的 cancelOrder
之类的注释不起作用,因为订单对象在那里不可用(SpEL 将能够引用函数参数,但订单是不作为参数传递!)
我能想到的唯一剩下的可能性是在继续业务逻辑之前显式调用授权函数:
fun cancelOrder(id: Long,principal: Principal) {
val orderId = OrderId(id)
val order = orderRepository.findById(orderId)
checkPolicy(principal,order,Context.Complaint) // ?
order.cancel()
}
具体采用什么ABAC服务器(如OPA、AuthzForce等)还没有定论,但这个层面的解决方案应该独立于它。
对于我概括为 checkPolicy
的内容是否有通用方法?是否有典型的“Spring 方法”来集成外部策略决策点?或者有没有更好的解决方案?
解决方法
选项 1 - 如果您可以修改 Order 类:
您可以直接在 @PreAuthorize
方法上应用 Order#cancel()
,这样您就可以像 checkPolicy
一样访问 order
对象(使用 #this
) :
class Order {
//...
@PreAuthorize("hasPermission(#this,T(fully.qualified.Context).Complaint)")
fun cancel() {...}
}
如果您的 Context.Complaint
常量是一个字符串,那么在上面的 Spel 表达式中放置字符串值(例如 'complaint'
?)而不是 T(...Context).Complaint
可能更具可读性。
选项 2 - 如果您无法修改 Order 类:
您可以将 order.cancel
的调用包装在一个中间函数中并在其上放置注释:
fun cancelOrder(id: Long,principal: Principal) {
val orderId = OrderId(id)
val order = orderRepository.findById(orderId)
secured_cancelOrder(order)
}
@PreAuthorize("hasPermission(#order,T(fully.qualified.Context).Complaint)")
fun secured_cancelOrder(order: Order) {
order.cancel()
}
然后你必须实现一个自定义的 PermissionEvaluator(Spring Security):
class AbacPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication,Object targetDomainObject,Object permission) {
log.info("check permission '{}' for user '{}' for target '{}'",permission,authentication.getPrincipal(),targetDomainObject)
var principal = authentication.getPrincipal()
// Make the authorization request to the PDP with subject attributes from principal,action attribute from permission and resource attributes from targetDomainObject
// ...
// return true if PDP's decision is Permit,else false.
}
}
因此您应该从 order
参数中获取 targetDomainObject
对象。但是,通过将 @PreAuthorize
表达式更改为如下所示,使用属性映射会更通用(ABAC 样式) - 较少特定于业务:
我假设 Order 类至少有一些 id
属性,但您可以有其他属性。
在选项 1 的情况下:
@PreAuthorize("hasPermission(T(java.util.Map).of('resource-id',#this.id),T(fully.qualified.Context).Complaint)")
在选项 2 的情况下:
@PreAuthorize("hasPermission(T(java.util.Map).of('resource-id',#order.id),T(fully.qualified.Context).Complaint)")
最后,在您的应用程序上下文中配置 PermissionEvaluator:
<bean id="abacPermEvaluator" class="fully.qualified.AbacPermissionEvaluator">
<!-- properties -->
</bean>
<security:global-method-security pre-post-annotations="enabled">
<security:expression-handler>
<bean class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<property name="permissionEvaluator" ref="abacPermEvaluator"/>
<!-- other properties like permissionCacheOptimizer-->
</bean>
</security:expression-handler>
</security:global-method-security>
你可以像往常一样用注解做同样的配置。