授权架构

本节介绍适用于授权的 Spring Security 架构。

权限

Authentication 讨论了所有 Authentication 实现如何存储 GrantedAuthority 对象列表。这些代表已授予主体的人员权限。GrantedAuthority 对象由 AuthenticationManager 插入 Authentication 对象,并在稍后由 AccessDecisionManager 实例在做出授权决策时读取。

GrantedAuthority 接口只有一个方法

String getAuthority();

此方法由 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 来配置授权规则以使用不同的前缀,如下所示

自定义 MethodSecurityExpressionHandler
  • Java

  • Kotlin

  • Xml

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("MYPREFIX_");
}
companion object {
	@Bean
	fun grantedAuthorityDefaults() : GrantedAuthorityDefaults {
		return GrantedAuthorityDefaults("MYPREFIX_");
	}
}
<bean id="grantedAuthorityDefaults" class="org.springframework.security.config.core.GrantedAuthorityDefaults">
	<constructor-arg value="MYPREFIX_"/>
</bean>

您使用 static 方法公开 GrantedAuthorityDefaults,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它。

调用处理

Spring Security 提供拦截器来控制对安全对象的访问,例如方法调用或 Web 请求。AuthorizationManager 实例会对是否允许调用继续进行进行预调用决策。此外,AuthorizationManager 实例还会对是否允许返回给定值进行调用后决策。

AuthorizationManager

AuthorizationManager 取代了 AccessDecisionManagerAccessDecisionVoter

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

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

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

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

AuthorizationManagercheck 方法会接收所有它做出授权决策所需的相关信息。特别是,传递安全的 Object 使得可以检查实际安全对象调用中包含的参数。例如,假设安全对象是 MethodInvocation。可以轻松地查询 MethodInvocation 中的任何 Customer 参数,然后在 AuthorizationManager 中实现某种安全逻辑,以确保主体被允许对该客户进行操作。预期实现会在授予访问权限时返回正向的 AuthorizationDecision,拒绝访问时返回负向的 AuthorizationDecision,以及在避免做出决策时返回 null 的 AuthorizationDecision

verify 调用 check,并在 AuthorizationDecision 为负的情况下随后抛出 AccessDeniedException

基于委托的 AuthorizationManager 实现

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

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

Authorization Manager 实现 说明了相关的类。

authorizationhierarchy
图 1. Authorization Manager 实现

使用这种方法,可以在授权决策上轮询 AuthorizationManager 实现的组合。

AuthorityAuthorizationManager

Spring Security 提供的最常见的 AuthorizationManagerAuthorityAuthorizationManager。它配置了一组给定的权限,以便在当前 Authentication 上查找。如果 Authentication 包含任何配置的权限,它将返回正向的 AuthorizationDecision。否则,它将返回负向的 AuthorizationDecision

AuthenticatedAuthorizationManager

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

AuthorizationManagers

AuthorizationManagers 中还有一些有用的静态工厂,用于将各个 AuthorizationManager 组合成更复杂的表达式。

自定义 Authorization Managers

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

您会在 Spring 网站上找到一篇关于如何使用旧版 AccessDecisionVoter 实时拒绝其帐户已被暂停的用户访问权限的博文。您可以通过实现 AuthorizationManager 来实现相同的结果。

适配 AccessDecisionManager 和 AccessDecisionVoters

AuthorizationManager 之前,Spring Security 发布了AccessDecisionManagerAccessDecisionVoter

在某些情况下,例如迁移旧版应用程序,可能需要引入一个调用 AccessDecisionManagerAccessDecisionVoterAuthorizationManager

要调用现有的 AccessDecisionManager,您可以执行以下操作

适配 AccessDecisionManager
  • Java

@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);
    }
}

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

或者仅调用 AccessDecisionVoter,您可以执行以下操作

适配 AccessDecisionVoter
  • Java

@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;
    }
}

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

角色层次结构

一个常见的要求是应用程序中的特定角色应该自动“包含”其他角色。例如,在一个具有“管理员”和“用户”角色概念的应用程序中,您可能希望管理员能够执行普通用户可以执行的所有操作。为此,您可以确保所有管理员用户也都被分配了“用户”角色。或者,您可以修改每个需要“用户”角色的访问约束,以使其也包含“管理员”角色。如果您的应用程序中有很多不同的角色,这可能会变得非常复杂。

使用角色层次结构允许您配置哪些角色(或权限)应该包含其他角色。这在基于过滤器的授权中(在 HttpSecurity#authorizeHttpRequests 中)和基于方法的授权中(通过 DefaultMethodSecurityExpressionHandler 用于前置/后置注解,SecuredAuthorizationManager 用于 @Secured 以及 Jsr250AuthorizationManager 用于 JSR-250 注解)得到支持。您可以通过以下方式一次配置所有这些行为

角色层次结构配置
  • Java

  • Xml

@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;
}
<bean id="roleHierarchy"
		class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy">
	<constructor-arg>
		<value>
			ROLE_ADMIN > ROLE_STAFF
			ROLE_STAFF > ROLE_USER
			ROLE_USER > ROLE_GUEST
		</value>
	</constructor-arg>
</bean>

<!-- and, if using method security also add -->
<bean id="methodSecurityExpressionHandler"
        class="org.springframework.security.access.expression.method.MethodSecurityExpressionHandler">
    <property ref="roleHierarchy"/>
</bean>

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

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

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

旧版授权组件

Spring Security 包含一些旧版组件。由于它们尚未删除,因此出于历史目的包含了文档。上面列出了它们的推荐替代方案。

AccessDecisionManager

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

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

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

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

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

基于投票的 AccessDecisionManager 实现

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

下图显示了 AccessDecisionManager 接口

access decision voting
图 2. 投票决策管理器

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

AccessDecisionVoter 接口有三个方法

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

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

具体实现返回一个 int,可能的值反映在名为 ACCESS_ABSTAINACCESS_DENIEDACCESS_GRANTEDAccessDecisionVoter 静态字段中。如果投票实现对授权决策没有意见,则返回 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 返回一个与一个或多个以 ROLE_ 前缀开头的 ConfigAttributes 完全相同的 String 表示形式(来自 getAuthority() 方法),则它会投票授予访问权限。如果没有与任何以 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配置属性来实现。