授权架构

本节介绍适用于授权的 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

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

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

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

AuthorizationManagercheck 方法传递了做出授权决策所需的所有相关信息。特别是,传递安全的 Object 可以检查实际安全对象调用中包含的参数。例如,假设安全对象是 MethodInvocation。可以轻松地查询 MethodInvocation 以获取任何 Customer 参数,然后在 AuthorizationManager 中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果授予访问权限,则实现应返回正 AuthorizationDecision;如果拒绝访问权限,则实现应返回负 AuthorizationDecision;如果弃权做出决策,则实现应返回空 AuthorizationDecision

verify 调用 check,并在 AuthorizationDecision 为负时随后抛出 AccessDeniedException

基于委托的 AuthorizationManager 实现

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

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

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

authorizationhierarchy
图 1. 授权管理器实现

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

AuthorityAuthorizationManager

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

AuthenticatedAuthorizationManager

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

AuthorizationManagers

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

自定义授权管理器

显然,您还可以实现一个自定义 AuthorizationManager,并且可以在其中放置所需的任何访问控制逻辑。它可能是特定于您的应用程序(与业务逻辑相关)的,或者它可能实现一些安全管理逻辑。例如,您可以创建一个可以查询开放策略代理或您自己的授权数据库的实现。

您会在 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

分层角色

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

使用角色层次结构允许您配置哪些角色(或权限)应包含其他角色。Spring Security 的 RoleVoter 的扩展版本 RoleHierarchyVoter 使用 RoleHierarchy 进行配置,从中获取分配给用户的全部“可达权限”。典型的配置可能如下所示

分层角色配置
  • 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 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>
RoleHierarchy bean 配置尚未移植到 @EnableMethodSecurity。因此,此示例使用 AccessDecisionVoter。如果您需要方法安全性的 RoleHierarchy 支持,请继续使用 @EnableGlobalMethodSecurity,直到 github.com/spring-projects/spring-security/issues/12783 完成。

这里我们在一个分层结构中具有四个角色 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST。使用 ROLE_ADMIN 进行身份验证的用户在针对调用上述 RoleHierarchyVoter 而调整的 AuthorizationManager 评估安全约束时,将表现得好像他们具有所有四个角色。> 符号可以理解为“包含”。

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

旧版授权组件

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

AbstractSecurityInterceptor 在启动时会调用 supports(ConfigAttribute) 方法,以确定 AccessDecisionManager 是否可以处理传递的 ConfigAttribute。安全拦截器实现会调用 supports(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,其返回一个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配置属性实现。