跳到主要内容

授权架构

QWen Max 中英对照 Authorization Architecture

本节描述适用于授权的Spring Security架构。

Authorities

认证讨论了所有 Authentication 实现如何存储 GrantedAuthority 对象的列表。这些对象代表授予主体的权限。GrantedAuthority 对象由 AuthenticationManager 插入到 Authentication 对象中,并在进行授权决策时由 AccessDecisionManager 实例读取。

GrantedAuthority 接口只有一个方法:

String getAuthority();
java

此方法由 AuthorizationManager 实例使用,以获取 GrantedAuthority 的精确 String 表示。通过返回 String 表示形式,大多数 AuthorizationManager 实现可以轻松“读取” GrantedAuthority。如果 GrantedAuthority 不能被精确地表示为 String,则该 GrantedAuthority 被认为是“复杂的”,并且 getAuthority() 必须返回 null

一个复杂的 GrantedAuthority 的例子是一个存储了对不同客户账号号码适用的操作和权限阈值列表的实现。将这个复杂的 GrantedAuthority 表示为一个 String 会非常困难。因此,getAuthority() 方法应该返回 null。这向任何 AuthorizationManager 表明它需要支持特定的 GrantedAuthority 实现来理解其内容。

Spring Security 包含一个具体的 GrantedAuthority 实现:SimpleGrantedAuthority。这个实现允许任何用户指定的 String 转换为 GrantedAuthority。安全架构中包含的所有 AuthenticationProvider 实例都使用 SimpleGrantedAuthority 来填充 Authentication 对象。

默认情况下,基于角色的授权规则包含 ROLE_ 作为前缀。这意味着,如果有一个授权规则要求安全上下文具有 "USER" 角色,Spring Security 默认会查找返回 "ROLE_USER" 的 GrantedAuthority#getAuthority

你可以使用 GrantedAuthorityDefaults 来自定义这个设置。GrantedAuthorityDefaults 存在的目的是允许自定义用于基于角色的授权规则的前缀。

您可以配置授权规则以使用不同的前缀,方法是暴露一个 GrantedAuthorityDefaults bean,如下所示:

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
java
提示

你通过使用 static 方法来暴露 GrantedAuthorityDefaults,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它。

调用处理

Spring Security 提供了拦截器来控制对安全对象(如方法调用或 web 请求)的访问。AuthorizationManager 实例会在调用前决定是否允许继续进行。同样,AuthorizationManager 实例也会在调用后决定是否可以返回给定的值。

授权管理器

AuthorizationManager 取代了 AccessDecisionManager 和 AccessDecisionVoter

自定义 AccessDecisionManagerAccessDecisionVoter 的应用程序应改为使用 AuthorizationManager

AuthorizationManager 由 Spring Security 的基于请求的基于方法的基于消息的 授权组件调用,并负责做出最终的访问控制决策。AuthorizationManager 接口包含两个方法:

AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);

default void verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
// ...
}
java

AuthorizationManagercheck 方法会传递所有相关信息,以便做出授权决策。特别是,传递安全 Object 可以让实际安全对象调用中包含的参数被检查。例如,假设安全对象是一个 MethodInvocation。可以很容易地查询 MethodInvocation 中的任何 Customer 参数,然后在 AuthorizationManager 中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果访问被授予,实现应该返回一个正向的 AuthorizationDecision;如果访问被拒绝,则返回负向的 AuthorizationDecision;当不做出决定时,返回 null AuthorizationDecision

verify 调用 check,并在 AuthorizationDecision 为否定的情况下抛出 AccessDeniedException

基于委托的 AuthorizationManager 实现

虽然用户可以实现自己的 AuthorizationManager 来控制授权的所有方面,但 Spring Security 提供了一个委托 AuthorizationManager,它可以与各个 AuthorizationManager 协作。

RequestMatcherDelegatingAuthorizationManager 会将请求与最合适的委托 AuthorizationManager 进行匹配。对于方法安全性,可以使用 AuthorizationManagerBeforeMethodInterceptorAuthorizationManagerAfterMethodInterceptor

授权管理器实现 说明了相关类。

authorizationhierarchy

图 1. 授权管理器实现

使用这种方法,可以对 AuthorizationManager 实现的组合进行轮询以做出授权决策。

AuthorityAuthorizationManager

最常用的 AuthorizationManager 是 Spring Security 提供的 AuthorityAuthorizationManager。它配置有一组特定的权限,用于在当前 Authentication 中查找。如果 Authentication 包含任何配置的权限,它将返回肯定的 AuthorizationDecision。否则,它将返回否定的 AuthorizationDecision

AuthenticatedAuthorizationManager

另一个管理器是 AuthenticatedAuthorizationManager。它可以用来区分匿名用户、完全认证用户和记住我认证的用户。许多网站允许在记住我认证的情况下进行某些有限的访问,但要求用户通过登录来确认其身份以获得完全访问权限。

AuthorizationManagers

AuthorizationManagers 中也有一些有用的静态工厂方法,可以将单个的 AuthorizationManager 组合成更复杂的表达式。

自定义授权管理器

显然,你也可以实现一个自定义的 AuthorizationManager,并且可以在其中放入你想要的任何访问控制逻辑。它可能是特定于你的应用程序(与业务逻辑相关),或者可能实现一些安全管理逻辑。例如,你可以创建一个实现来查询 Open Policy Agent 或你自己的授权数据库。

提示

你会在 Spring 网站上找到一篇博客文章,其中描述了如何使用旧版的 AccessDecisionVoter 来实时拒绝已被暂停帐户的用户的访问。你也可以通过实现 AuthorizationManager 来达到同样的效果。

适配 AccessDecisionManager 和 AccessDecisionVoters

AuthorizationManager 之前,Spring Security 发布了 AccessDecisionManager 和 AccessDecisionVoter

在某些情况下,比如迁移旧应用程序时,可能需要引入一个 AuthorizationManager,它会调用一个 AccessDecisionManagerAccessDecisionVoter

要调用现有的 AccessDecisionManager,你可以这样做:

@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionManager accessDecisionManager;
private final SecurityMetadataSource securityMetadataSource;

@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
try {
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
return new AuthorizationDecision(true);
} catch (AccessDeniedException ex) {
return new AuthorizationDecision(false);
}
}

@Override
public void verify(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
}
}
java

然后将其连接到你的 SecurityFilterChain 中。

或者,如果你只想调用一个 AccessDecisionVoter,你可以这样做:

@Component
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionVoter accessDecisionVoter;
private final SecurityMetadataSource securityMetadataSource;

@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
switch (decision) {
case ACCESS_GRANTED:
return new AuthorizationDecision(true);
case ACCESS_DENIED:
return new AuthorizationDecision(false);
}
return null;
}
}
java

然后将其连接到你的 SecurityFilterChain 中。

层级角色

在应用程序中,通常需要某个特定角色自动“包含”其他角色。例如,在具有“管理员”和“用户”角色概念的应用程序中,你可能希望管理员能够执行普通用户可以做的所有操作。为了实现这一点,你可以确保所有管理员用户也被分配了“用户”角色。或者,你可以修改每个需要“用户”角色的访问约束,使其也包括“管理员”角色。如果你的应用程序中有许多不同的角色,这样做可能会变得相当复杂。

使用角色层次结构允许你配置哪些角色(或权限)应包含其他角色。这在 HttpSecurity#authorizeHttpRequests 中的基于过滤器的授权以及通过 DefaultMethodSecurityExpressionHandler 对预后注解的方法级授权、通过 SecuredAuthorizationManager@Secured 以及通过 Jsr250AuthorizationManager 对 JSR-250 注解的支持中都是可行的。你可以通过以下方式一次性为所有这些配置行为:

@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("STAFF")
.role("STAFF").implies("USER")
.role("USER").implies("GUEST")
.build();
}

// and, if using pre-post method security also add
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy);
return expressionHandler;
}
java

这里我们有一个层级结构中的四个角色 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST。一个以 ROLE_ADMIN 身份验证的用户,在针对任何基于过滤器或方法的规则进行安全约束评估时,将表现得好像他们拥有所有四个角色一样。

提示

> 符号可以理解为“包含”的意思。

角色层次结构提供了一种方便的手段,用于简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量。对于更复杂的需求,您可能希望定义应用程序所需的具体访问权限与分配给用户的角色之间的逻辑映射,在加载用户信息时在这两者之间进行转换。

旧版授权组件

备注

Spring Security 包含了一些遗留组件。由于它们尚未被移除,因此文档中包含了这些内容以作历史参考。推荐的替代方案已在上方列出。

The AccessDecisionManager

AccessDecisionManagerAbstractSecurityInterceptor 调用,负责做出最终的访问控制决策。AccessDecisionManager 接口包含三个方法:

void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);
java

AccessDecisionManagerdecide 方法传递了做出授权决策所需的所有相关信息。特别是,传递受保护的 Object 可以让实际受保护对象调用中包含的参数被检查。例如,假设受保护的对象是一个 MethodInvocation。你可以查询 MethodInvocation 中的任何 Customer 参数,然后在 AccessDecisionManager 中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果访问被拒绝,实现应该抛出一个 AccessDeniedException

supports(ConfigAttribute) 方法在启动时由 AbstractSecurityInterceptor 调用,以确定 AccessDecisionManager 是否可以处理传递的 ConfigAttributesupports(Class) 方法由安全拦截器实现调用,以确保配置的 AccessDecisionManager 支持安全拦截器呈现的安全对象类型。

基于投票的 AccessDecisionManager 实现

虽然用户可以实现自己的 AccessDecisionManager 来控制授权的所有方面,但 Spring Security 包括了几个基于投票的 AccessDecisionManager 实现。投票决策管理器 描述了相关的类。

下图显示了 AccessDecisionManager 接口:

访问决策投票

图 2. 投票决策管理器

通过使用这种方法,一系列的 AccessDecisionVoter 实现会在授权决策时被轮询。然后,AccessDecisionManager 根据对投票的评估决定是否抛出 AccessDeniedException

AccessDecisionVoter 接口有三个方法:

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);
java

具体实现返回一个 int,可能的值反映在 AccessDecisionVoter 的静态字段中,这些字段名为 ACCESS_ABSTAINACCESS_DENIEDACCESS_GRANTED。如果投票实现对授权决策没有意见,则返回 ACCESS_ABSTAIN。如果它有意见,则必须返回 ACCESS_DENIEDACCESS_GRANTED

Spring Security 提供了三种具体的 AccessDecisionManager 实现来统计投票。ConsensusBased 实现根据非弃权票的共识授予或拒绝访问。提供了属性来控制在票数相等或所有票都是弃权的情况下应该如何处理。AffirmativeBased 实现如果收到一个或多个 ACCESS_GRANTED 投票则授予访问权限(换句话说,只要有一个同意票,拒绝票将被忽略)。与 ConsensusBased 实现一样,有一个参数可以控制当所有投票者都弃权时的行为。UnanimousBased 提供者期望一致的 ACCESS_GRANTED 投票才能授予访问权限,同时忽略弃权票。如果有任何 ACCESS_DENIED 投票,则会拒绝访问。与其他实现一样,也有一个参数可以控制当所有投票者都弃权时的行为。

你可以实现一个自定义的 AccessDecisionManager,以不同的方式计算投票。例如,来自特定 AccessDecisionVoter 的投票可能会获得额外的权重,而来自某个投票者的拒绝投票可能会产生否决效果。

RoleVoter

Spring Security 提供的最常用的 AccessDecisionVoterRoleVoter,它将配置属性视为角色名称,如果用户被分配了该角色,则投票授予访问权限。

如果任何 ConfigAttributeROLE_ 前缀开头,它就会投票。如果有 GrantedAuthority 返回的 String 表示(来自 getAuthority() 方法)与一个或多个以 ROLE_ 前缀开头的 ConfigAttributes 完全匹配,它就会投票授予访问权限。如果没有以 ROLE_ 开头的 ConfigAttribute 完全匹配,RoleVoter 会投票拒绝访问。如果没有 ConfigAttributeROLE_ 开头,投票者将弃权。

AuthenticatedVoter

另一个我们隐式见过的投票器是 AuthenticatedVoter,它可以用来区分匿名用户、完全认证用户和记住我认证用户。许多网站允许在记住我认证下进行某些有限的访问,但要求用户通过登录确认其身份以获得完全访问权限。

当我们使用 IS_AUTHENTICATED_ANONYMOUSLY 属性授予匿名访问时,此属性由 AuthenticatedVoter 处理。更多信息,请参见 AuthenticatedVoter

自定义投票者

你也可以实现一个自定义的 AccessDecisionVoter,并在其中放入你想要的任何访问控制逻辑。这可能是特定于你的应用程序(与业务逻辑相关),也可能是实现一些安全管理逻辑。例如,在 Spring 网站上,你可以找到一篇博客文章,该文章描述了如何使用投票器实时拒绝已被暂停帐户的用户的访问。

after invocation

图 3. 调用后的实现

像 Spring Security 的许多其他部分一样,AfterInvocationManager 有一个具体的实现,即 AfterInvocationProviderManager,它会轮询一个 AfterInvocationProvider 列表。每个 AfterInvocationProvider 都可以修改返回对象或抛出 AccessDeniedException。实际上,多个提供者可以修改该对象,因为前一个提供者的处理结果会被传递给列表中的下一个提供者。

请注意,如果你使用 AfterInvocationManager,你仍然需要配置属性,以便 MethodSecurityInterceptorAccessDecisionManager 允许操作。如果你使用的是典型的 Spring Security 包含的 AccessDecisionManager 实现,那么对于特定的安全方法调用没有定义配置属性将导致每个 AccessDecisionVoter 弃权。反过来,如果 AccessDecisionManager 属性“allowIfAllAbstainDecisions”为 false,则会抛出 AccessDeniedException。你可以通过以下两种方式之一来避免这种潜在问题:(i) 将“allowIfAllAbstainDecisions”设置为 true(尽管通常不推荐这样做),或 (ii) 确保至少有一个配置属性,使 AccessDecisionVoter 投票授予访问权限。后一种(推荐)方法通常通过 ROLE_USERROLE_AUTHENTICATED 配置属性来实现。