授权架构
本节描述适用于授权的 Spring Security 架构。
权限
Authentication 讨论了所有 Authentication 实现如何存储 GrantedAuthority 对象列表。
这些代表已授予主体的权限。
GrantedAuthority 对象由 AuthenticationManager 插入到 Authentication 对象中,并在进行授权决策时由 AuthorizationManager 实例后续读取。
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 对象。
你可以使用 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>
|
您通过一个 |
调用处理
Spring Security 提供了用于控制对安全对象(例如方法调用或 Web 请求)访问的拦截器。
是否允许调用继续进行的前置调用决策由 AuthorizationManager 实例作出。
此外,是否允许返回特定值的后置调用决策也由 AuthorizationManager 实例作出。
授权管理器
AuthorizationManager 取代了 AccessDecisionManager 和 AccessDecisionVoter。
自定义 AccessDecisionManager 或 AccessDecisionVoter 的应用程序建议改用 AuthorizationManager。
AuthorizationManager 由 Spring Security 的基于请求的、基于方法的和基于消息的授权组件调用,负责做出最终的访问控制决策。
AuthorizationManager 接口包含两个方法:
AuthorizationResult authorize(Supplier<Authentication> authentication, Object secureObject);
default void verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
// ...
}
AuthorizationManager的authorize方法会接收做出授权决策所需的所有相关信息。
特别是,传递安全的Object可以检查实际安全对象调用中包含的参数。
例如,假设安全对象是一个MethodInvocation。
可以轻松地查询MethodInvocation以获取任何Customer参数,然后在AuthorizationManager中实现某种安全逻辑,以确保主体被允许操作该客户。
如果授予访问权限,实现应返回正的AuthorizationDecision;如果拒绝访问,则返回负的AuthorizationDecision;如果放弃做出决定,则返回 null 的AuthorizationDecision。
verify 调用 authorize,并在 AccessDeniedException 为否定时抛出 AuthorizationDecision。
基于委托的 AuthorizationManager 实现
虽然用户可以实现自己的 AuthorizationManager 来控制授权的所有方面,但 Spring Security 自带了一个委托型的 AuthorizationManager,它可以与各个独立的 AuthorizationManager 协作。
RequestMatcherDelegatingAuthorizationManager 将使用最合适的委托 AuthorizationManager 来匹配请求。
对于方法级安全,您可以使用 AuthorizationManagerBeforeMethodInterceptor 和 AuthorizationManagerAfterMethodInterceptor。
授权管理器实现 展示了相关类。
使用这种方法,可以在授权决策时轮询多个 AuthorizationManager 实现的组合。
创建 AuthorizationManager 实例
AuthorizationManagerFactory 接口(在 Spring Security 7.0 中引入)用于在 基于请求的 和 基于方法的 授权组件中创建通用的 AuthorizationManager。
以下是 AuthorizationManagerFactory 接口的示意图:
public interface AuthorizationManagerFactory<T> {
AuthorizationManager<T> permitAll();
AuthorizationManager<T> denyAll();
AuthorizationManager<T> hasRole(String role);
AuthorizationManager<T> hasAnyRole(String... roles);
AuthorizationManager<T> hasAllRoles(String... roles);
AuthorizationManager<T> hasAuthority(String authority);
AuthorizationManager<T> hasAnyAuthority(String... authorities);
AuthorizationManager<T> hasAllAuthorities(String... authorities);
AuthorizationManager<T> authenticated();
AuthorizationManager<T> fullyAuthenticated();
AuthorizationManager<T> rememberMe();
AuthorizationManager<T> anonymous();
}
默认实现是 DefaultAuthorizationManagerFactory,它允许自定义提供给工厂创建的 AuthorizationManager 的 rolePrefix(默认为 "ROLE_")、RoleHierarchy 和 AuthenticationTrustManager。
为了自定义 Spring Security 使用的默认实例,只需像下面示例一样发布一个 bean 即可:
-
Java
-
Kotlin
-
Xml
@Bean
<T> AuthorizationManagerFactory<T> authorizationManagerFactory() {
DefaultAuthorizationManagerFactory<T> authorizationManagerFactory =
new DefaultAuthorizationManagerFactory<>();
authorizationManagerFactory.setTrustResolver(getAuthenticationTrustResolver());
authorizationManagerFactory.setRoleHierarchy(getRoleHierarchy());
authorizationManagerFactory.setRolePrefix("role_");
return authorizationManagerFactory;
}
@Bean
fun <T> authorizationManagerFactory(): AuthorizationManagerFactory<T> {
val authorizationManagerFactory = DefaultAuthorizationManagerFactory<T>()
authorizationManagerFactory.setTrustResolver(getAuthenticationTrustResolver())
authorizationManagerFactory.setRoleHierarchy(getRoleHierarchy())
authorizationManagerFactory.setRolePrefix("role_")
return authorizationManagerFactory
}
<b:bean id="authorizationManagerFactory" class="org.springframework.security.authorization.DefaultAuthorizationManagerFactory">
<b:property name="trustResolver" ref="authenticationTrustResolver"/>
<b:property name="roleHierarchy" ref="roleHierarchy"/>
<b:property name="rolePrefix" value="role_"/>
</b:bean>
除了简单地自定义默认的 AuthorizationManagerFactory 实例外,您还可以提供自己的实现,以完全自定义该工厂所创建的实例,并提供您自己的实现。
| 实际的接口为所有工厂方法提供了默认实现,这使得自定义实现只需实现需要定制的方法即可。 |
授权管理器
在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 AuthorizationResult authorize(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 AuthorizationResult authorize(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 中。
层次化角色
在应用程序中,通常会要求某个特定角色自动“包含”其他角色。 例如,在一个具有“管理员(admin)”和“用户(user)”角色概念的应用程序中,你可能希望管理员能够执行普通用户所能做的所有操作。 要实现这一点,你可以确保所有管理员用户同时也被分配了“用户”角色。 或者,你也可以修改每一个需要“用户”角色的访问控制约束,使其同时包含“管理员”角色。 如果你的应用程序中有大量不同的角色,这种方法可能会变得相当复杂。
使用角色层级(role-hierarchy)可以配置哪些角色(或权限)应包含其他角色。
这在基于过滤器的授权中通过 HttpSecurity#authorizeHttpRequests 得到支持,
在方法级授权中则通过以下方式支持:
DefaultMethodSecurityExpressionHandler 用于 pre-post 注解,
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 包含一些遗留组件。 由于它们尚未被移除,此处包含相关文档仅供历史参考。 其推荐的替代方案已在上方列出。 |
在访问旧版授权组件时,请同时包含 spring-security-access 依赖,如下所示:
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-access</artifactId>
</dependency>
implementation('org.springframework.security:spring-security-access')
访问决策管理器
AccessDecisionManager 由 AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策。
AccessDecisionManager 接口包含三个方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
decide 的 AccessDecisionManager 方法会接收到做出授权决策所需的所有相关信息。
特别是,传入受保护的 Object 可以让实际受保护对象调用中包含的参数被检查。
例如,假设受保护对象是一个 MethodInvocation。
您可以查询该 MethodInvocation 中是否包含任何 Customer 参数,然后在 AccessDecisionManager 中实现某种安全逻辑,以确保主体(principal)被允许对该客户执行操作。
如果拒绝访问,实现应抛出一个 AccessDeniedException。
supports(ConfigAttribute) 方法在启动时由 AbstractSecurityInterceptor 调用,以确定 AccessDecisionManager 是否能够处理传入的 ConfigAttribute。
supports(Class) 方法由安全拦截器的实现调用,以确保所配置的 AccessDecisionManager 支持该安全拦截器所提供的安全对象类型。
基于投票的 AccessDecisionManager 实现
虽然用户可以实现自己的AccessDecisionManager来控制授权的各个方面,但Spring Security也提供了几种基于投票机制的AccessDecisionManager实现。
投票决策管理器介绍了相关的类。
下图展示了 AccessDecisionManager 接口:
通过使用这种方法,会轮询一系列 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的投票可能会获得额外的权重,而某个特定投票者投出的拒绝票则可能具有否决效果。
角色投票器
Spring Security 提供的最常用的 AccessDecisionVoter 是 RoleVoter,它将配置属性视为角色名称,如果用户已被分配该角色,则投票授予访问权限。
如果任何ConfigAttribute以ROLE_前缀开头,则进行投票。
如果存在一个GrantedAuthority返回与一个或多个以ROLE_前缀开头的ConfigAttributes完全相等的String表示(来自getAuthority()方法),则投票授予访问权限。
如果没有与以ROLE_开头的任何ConfigAttribute完全匹配,则RoleVoter投票拒绝访问。
如果没有ConfigAttribute以ROLE_开头,则投票者弃权。
已认证投票者
我们之前隐式见到的另一个投票器是AuthenticatedVoter,它可用于区分匿名用户、完全认证用户和“记住我”认证用户。
许多网站允许在“记住我”认证下进行某些有限的访问,但要求用户通过登录来确认身份,以获得完整访问权限。
当我们使用 IS_AUTHENTICATED_ANONYMOUSLY 属性授予匿名访问权限时,该属性由 AuthenticatedVoter 处理。
有关更多信息,请参阅
AuthenticatedVoter。
自定义投票器
你也可以实现一个自定义的 AccessDecisionVoter,并在其中编写几乎任何你想要的访问控制逻辑。
这种逻辑可能与你的应用程序特定相关(与业务逻辑相关),也可能实现某些安全管理逻辑。
例如,在 Spring 官方网站上,你可以找到一篇博客文章,其中描述了如何使用投票器实时拒绝已停用账户用户的访问。
与 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 配置属性来实现。