授权架构
本节描述适用于授权的Spring Security架构。
Authorities
认证讨论了所有 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,如下所示:
- 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
取代了 AccessDecisionManager 和 AccessDecisionVoter。
自定义 AccessDecisionManager
或 AccessDecisionVoter
的应用程序应改为使用 AuthorizationManager。
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
default void verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
// ...
}
AuthorizationManager
的 check
方法会传递所有相关信息,以便做出授权决策。特别是,传递安全 Object
可以让实际安全对象调用中包含的参数被检查。例如,假设安全对象是一个 MethodInvocation
。可以很容易地查询 MethodInvocation
中的任何 Customer
参数,然后在 AuthorizationManager
中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果访问被授予,实现应该返回一个正向的 AuthorizationDecision
;如果访问被拒绝,则返回负向的 AuthorizationDecision
;当不做出决定时,返回 null AuthorizationDecision
。
verify
调用 check
,并在 AuthorizationDecision
为否定的情况下抛出 AccessDeniedException
。
基于委托的 AuthorizationManager 实现
虽然用户可以实现自己的 AuthorizationManager
来控制授权的所有方面,但 Spring Security 提供了一个委托 AuthorizationManager
,它可以与各个 AuthorizationManager
协作。
RequestMatcherDelegatingAuthorizationManager
会将请求与最合适的委托 AuthorizationManager
进行匹配。对于方法安全性,可以使用 AuthorizationManagerBeforeMethodInterceptor
和 AuthorizationManagerAfterMethodInterceptor
。
授权管理器实现 说明了相关类。
图 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
,它会调用一个 AccessDecisionManager
或 AccessDecisionVoter
。
要调用现有的 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
,你可以这样做:
- 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 包含了一些遗留组件。由于它们尚未被移除,因此文档中包含了这些内容以作历史参考。推荐的替代方案已在上方列出。
The AccessDecisionManager
AccessDecisionManager
由 AbstractSecurityInterceptor
调用,负责做出最终的访问控制决策。AccessDecisionManager
接口包含三个方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
AccessDecisionManager
的 decide
方法传递了做出授权决策所需的所有相关信息。特别是,传递受保护的 Object
可以让实际受保护对象调用中包含的参数被检查。例如,假设受保护的对象是一个 MethodInvocation
。你可以查询 MethodInvocation
中的任何 Customer
参数,然后在 AccessDecisionManager
中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果访问被拒绝,实现应该抛出一个 AccessDeniedException
。
supports(ConfigAttribute)
方法在启动时由 AbstractSecurityInterceptor
调用,以确定 AccessDecisionManager
是否可以处理传递的 ConfigAttribute
。supports(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);
具体实现返回一个 int
,可能的值反映在 AccessDecisionVoter
的静态字段中,这些字段名为 ACCESS_ABSTAIN
、ACCESS_DENIED
和 ACCESS_GRANTED
。如果投票实现对授权决策没有意见,则返回 ACCESS_ABSTAIN
。如果它有意见,则必须返回 ACCESS_DENIED
或 ACCESS_GRANTED
。
Spring Security 提供了三种具体的 AccessDecisionManager
实现来统计投票。ConsensusBased
实现根据非弃权票的共识授予或拒绝访问。提供了属性来控制在票数相等或所有票都是弃权的情况下应该如何处理。AffirmativeBased
实现如果收到一个或多个 ACCESS_GRANTED
投票则授予访问权限(换句话说,只要有一个同意票,拒绝票将被忽略)。与 ConsensusBased
实现一样,有一个参数可以控制当所有投票者都弃权时的行为。UnanimousBased
提供者期望一致的 ACCESS_GRANTED
投票才能授予访问权限,同时忽略弃权票。如果有任何 ACCESS_DENIED
投票,则会拒绝访问。与其他实现一样,也有一个参数可以控制当所有投票者都弃权时的行为。
你可以实现一个自定义的 AccessDecisionManager
,以不同的方式计算投票。例如,来自特定 AccessDecisionVoter
的投票可能会获得额外的权重,而来自某个投票者的拒绝投票可能会产生否决效果。
RoleVoter
Spring Security 提供的最常用的 AccessDecisionVoter
是 RoleVoter
,它将配置属性视为角色名称,如果用户被分配了该角色,则投票授予访问权限。
如果任何 ConfigAttribute
以 ROLE_
前缀开头,它就会投票。如果有 GrantedAuthority
返回的 String
表示(来自 getAuthority()
方法)与一个或多个以 ROLE_
前缀开头的 ConfigAttributes
完全匹配,它就会投票授予访问权限。如果没有以 ROLE_
开头的 ConfigAttribute
完全匹配,RoleVoter
会投票拒绝访问。如果没有 ConfigAttribute
以 ROLE_
开头,投票者将弃权。
AuthenticatedVoter
另一个我们隐式见过的投票器是 AuthenticatedVoter
,它可以用来区分匿名用户、完全认证用户和记住我认证用户。许多网站允许在记住我认证下进行某些有限的访问,但要求用户通过登录确认其身份以获得完全访问权限。
当我们使用 IS_AUTHENTICATED_ANONYMOUSLY
属性授予匿名访问时,此属性由 AuthenticatedVoter
处理。更多信息,请参见 AuthenticatedVoter。
自定义投票者
你也可以实现一个自定义的 AccessDecisionVoter
,并在其中放入你想要的任何访问控制逻辑。这可能是特定于你的应用程序(与业务逻辑相关),也可能是实现一些安全管理逻辑。例如,在 Spring 网站上,你可以找到一篇博客文章,该文章描述了如何使用投票器实时拒绝已被暂停帐户的用户的访问。
图 3. 调用后的实现
像 Spring Security 的许多其他部分一样,AfterInvocationManager
有一个具体的实现,即 AfterInvocationProviderManager
,它会轮询一个 AfterInvocationProvider
列表。每个 AfterInvocationProvider
都可以修改返回对象或抛出 AccessDeniedException
。实际上,多个提供者可以修改该对象,因为前一个提供者的处理结果会被传递给列表中的下一个提供者。
请注意,如果你使用 AfterInvocationManager
,你仍然需要配置属性,以便 MethodSecurityInterceptor
的 AccessDecisionManager
允许操作。如果你使用的是典型的 Spring Security 包含的 AccessDecisionManager
实现,那么对于特定的安全方法调用没有定义配置属性将导致每个 AccessDecisionVoter
弃权。反过来,如果 AccessDecisionManager
属性“allowIfAllAbstainDecisions”为 false
,则会抛出 AccessDeniedException
。你可以通过以下两种方式之一来避免这种潜在问题:(i) 将“allowIfAllAbstainDecisions”设置为 true
(尽管通常不推荐这样做),或 (ii) 确保至少有一个配置属性,使 AccessDecisionVoter
投票授予访问权限。后一种(推荐)方法通常通过 ROLE_USER
或 ROLE_AUTHENTICATED
配置属性来实现。