Servlet 认证架构
本讨论在 Servlet 安全:全局概览 的基础上,描述了 Spring Security 在 Servlet 认证中使用的主要架构组件。如果您需要具体的流程来解释这些组件如何协同工作,请参阅 认证机制 特定章节。
-
SecurityContextHolder -
SecurityContextHolder是 Spring Security 存储 已认证 用户详细信息的地方。 -
SecurityContext - 从
SecurityContextHolder获取,包含当前已认证用户的Authentication。 -
Authentication - 可以作为
AuthenticationManager的输入,提供用户提供的凭据进行认证;也可以是SecurityContext中当前用户。 -
GrantedAuthority - 授予
Authentication中主体(即角色、范围等)的权限。 -
AuthenticationManager - 定义 Spring Security 过滤器如何执行 认证 的 API。
-
ProviderManager -
AuthenticationManager最常见的实现。 -
AuthenticationProvider - 由
ProviderManager使用,执行特定类型的认证。 -
使用
AuthenticationEntryPoint请求凭据 - 用于向客户端请求凭据(例如,重定向到登录页面,发送WWW-Authenticate响应等)。 -
AbstractAuthenticationProcessingFilter - 用于认证的基础
Filter。这也很好地展示了认证的高级流程以及各个组件如何协同工作。
SecurityContextHolder
Spring Security 认证模型的核心是 SecurityContextHolder。它包含 SecurityContext。
SecurityContextHolder 是 Spring Security 存储 已认证 用户详细信息的地方。Spring Security 不关心 SecurityContextHolder 是如何填充的。如果它包含一个值,则将其用作当前已认证的用户。
指示用户已认证的最简单方法是直接设置 SecurityContextHolder
SecurityContextHolder-
Java
-
Kotlin
SecurityContext context = SecurityContextHolder.createEmptyContext(); (1)
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); (2)
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); (3)
val context: SecurityContext = SecurityContextHolder.createEmptyContext() (1)
val authentication: Authentication = TestingAuthenticationToken("username", "password", "ROLE_USER") (2)
context.authentication = authentication
SecurityContextHolder.setContext(context) (3)
| 1 | 我们首先创建一个空的 SecurityContext。您应该创建一个新的 SecurityContext 实例,而不是使用 SecurityContextHolder.getContext().setAuthentication(authentication),以避免在多个线程之间发生竞争条件。 |
| 2 | 接下来,我们创建一个新的 Authentication 对象。Spring Security 不关心 SecurityContext 上设置的 Authentication 实现类型。这里,我们使用 TestingAuthenticationToken,因为它非常简单。更常见的生产场景是 UsernamePasswordAuthenticationToken(userDetails, password, authorities)。 |
| 3 | 最后,我们将 SecurityContext 设置在 SecurityContextHolder 上。Spring Security 使用此信息进行 授权。 |
要获取有关已认证主体的信息,请访问 SecurityContextHolder。
-
Java
-
Kotlin
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
val context = SecurityContextHolder.getContext()
val authentication = context.authentication
val username = authentication.name
val principal = authentication.principal
val authorities = authentication.authorities
默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些详细信息,这意味着 SecurityContext 始终可用于同一线程中的方法,即使 SecurityContext 未作为参数显式传递给这些方法。以这种方式使用 ThreadLocal 是非常安全的,只要您在处理完当前主体的请求后注意清除线程即可。Spring Security 的 FilterChainProxy 确保 SecurityContext 始终被清除。
有些应用程序并不完全适合使用 ThreadLocal,因为它们处理线程的方式特殊。例如,Swing 客户端可能希望 Java 虚拟机中的所有线程都使用相同的安全上下文。您可以在启动时为 SecurityContextHolder 配置策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用 SecurityContextHolder.MODE_GLOBAL 策略。其他应用程序可能希望由安全线程派生的线程也假定相同的安全身份。您可以通过使用 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 来实现此目的。您可以通过两种方式更改默认的 SecurityContextHolder.MODE_THREADLOCAL 模式。第一种是设置系统属性。第二种是调用 SecurityContextHolder 上的静态方法。大多数应用程序不需要更改默认设置。但是,如果您需要更改,请查看 SecurityContextHolder 的 JavaDoc 以了解更多信息。
SecurityContext
SecurityContext 从 SecurityContextHolder 获取。SecurityContext 包含一个 Authentication 对象。
Authentication
Authentication 接口在 Spring Security 中主要有两个用途
-
作为
AuthenticationManager的输入,提供用户提供的凭据以进行认证。在这种情况下,isAuthenticated()返回false。 -
表示当前已认证用户。您可以从 SecurityContext 获取当前的
Authentication。
Authentication 包含
-
principal:标识用户。使用用户名/密码进行认证时,这通常是UserDetails的实例。 -
credentials:通常是密码。在许多情况下,用户认证后会清除此项,以确保其不会泄露。 -
authorities:GrantedAuthority实例是授予用户的高级权限。两个示例是角色和范围。
它还配备了一个 AdditionalRequiredFactorsBuilder,允许您修改现有的 Authentication 实例并可能将其与另一个合并。这在需要从一个认证步骤(如表单登录)获取权限并将其应用于另一个认证步骤(如一次性令牌登录)的场景中非常有用,如下所示
-
Java
-
Kotlin
Authentication lastestResult = authenticationManager.authenticate(authenticationRequest);
Authentication previousResult = SecurityContextHolder.getContext().getAuthentication();
if (previousResult != null && previousResult.isAuthenticated()) {
lastestResult = lastestResult.toBuilder()
.authorities((a) -> a.addAll(previous.getAuthorities()))
.build();
}
var latestResult: Authentication = authenticationManager.authenticate(authenticationRequest)
val previousResult = SecurityContextHolder.getContext().authentication;
if (previousResult?.isAuthenticated == true) {
latestResult = latestResult.toBuilder().authorities { a ->
a.addAll(previousResult.authorities)
}.build()
}
GrantedAuthority
GrantedAuthority 实例是授予用户的高级权限。两个示例是角色和范围。
您可以从 Authentication.getAuthorities() 方法获取 GrantedAuthority 实例。此方法提供一个 GrantedAuthority 对象的 Collection。GrantedAuthority 顾名思义,是授予主体的权限。此类权限通常是“角色”,例如 ROLE_ADMINISTRATOR 或 ROLE_HR_SUPERVISOR。这些角色随后用于 Web 授权、方法授权和领域对象授权。Spring Security 的其他部分会解释这些权限并期望它们存在。在使用基于用户名/密码的认证时,GrantedAuthority 实例通常由 UserDetailsService 加载。
通常,GrantedAuthority 对象是应用程序范围的权限。它们不特定于给定的领域对象。因此,您不太可能拥有一个 GrantedAuthority 来表示对 Employee 对象编号 54 的权限,因为如果有数千个这样的权限,您很快就会耗尽内存(或者,至少会导致应用程序认证用户花费很长时间)。当然,Spring Security 明确设计用于处理这种常见需求,但您应该为此目的使用项目的领域对象安全功能。
AuthenticationManager
AuthenticationManager 是定义 Spring Security 过滤器如何执行 认证 的 API。返回的 Authentication 随后由调用 AuthenticationManager 的控制器(即 Spring Security 的 Filters 实例)设置到 SecurityContextHolder 上。如果您没有与 Spring Security 的 Filters 实例集成,您可以直接设置 SecurityContextHolder,并且不需要使用 AuthenticationManager。
虽然 AuthenticationManager 的实现可以是任何类型,但最常见的实现是 ProviderManager。
ProviderManager
ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager 将认证委托给一个 AuthenticationProvider 实例的 List。每个 AuthenticationProvider 都有机会指示认证应该成功、失败,或者指示它无法做出决定并允许下游的 AuthenticationProvider 做出决定。如果没有一个配置的 AuthenticationProvider 实例能够进行认证,则认证失败,并抛出 ProviderNotFoundException,这是一个特殊的 AuthenticationException,表示 ProviderManager 未配置为支持传递给它的 Authentication 类型。
实际上,每个 AuthenticationProvider 都知道如何执行特定类型的认证。例如,一个 AuthenticationProvider 可能能够验证用户名/密码,而另一个可能能够认证 SAML 断言。这使得每个 AuthenticationProvider 都能执行非常特定类型的认证,同时支持多种认证类型并仅暴露一个 AuthenticationManager bean。
ProviderManager 还允许配置一个可选的父级 AuthenticationManager,当没有任何 AuthenticationProvider 能够执行认证时,将咨询该父级。父级可以是任何类型的 AuthenticationManager,但它通常是 ProviderManager 的一个实例。
事实上,多个 ProviderManager 实例可能共享同一个父 AuthenticationManager。这在存在多个 SecurityFilterChain 实例,它们具有一些共同的认证(共享的父 AuthenticationManager),但也有不同的认证机制(不同的 ProviderManager 实例)的场景中相当常见。
默认情况下,ProviderManager 会尝试清除成功认证请求返回的 Authentication 对象中的任何敏感凭据信息。这可以防止密码等信息在 HttpSession 中保留的时间超过必要。
|
|
当您使用用户对象缓存时(例如,为了提高无状态应用程序的性能),这可能会导致问题。如果 Authentication 包含对缓存中对象的引用(例如 UserDetails 实例)并且其凭据被移除,则不再可能根据缓存的值进行认证。如果您使用缓存,则需要考虑这一点。一个显而易见的解决方案是首先复制对象,无论是在缓存实现中还是在创建返回的 Authentication 对象的 AuthenticationProvider 中。或者,您可以禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。请参阅 ProviderManager 类的 Javadoc。
AuthenticationProvider
您可以将多个 AuthenticationProvider 实例注入 ProviderManager。每个 AuthenticationProvider 执行特定类型的认证。例如,DaoAuthenticationProvider 支持基于用户名/密码的认证,而 JwtAuthenticationProvider 支持认证 JWT 令牌。
使用 AuthenticationEntryPoint 请求凭据
AuthenticationEntryPoint 用于发送 HTTP 响应,从客户端请求凭据。
有时,客户端会主动包含凭据(例如用户名和密码)来请求资源。在这种情况下,Spring Security 无需提供从客户端请求凭据的 HTTP 响应,因为它们已经包含在内。
在其他情况下,客户端对未经授权访问的资源发出未经认证的请求。在这种情况下,将使用 AuthenticationEntryPoint 的实现来向客户端请求凭据。AuthenticationEntryPoint 实现可能会执行 重定向到登录页面,响应 WWW-Authenticate 标头,或采取其他操作。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 用作认证用户凭据的基础 Filter。在凭据可以被认证之前,Spring Security 通常会使用 AuthenticationEntryPoint 来请求凭据。
接下来,AbstractAuthenticationProcessingFilter 可以认证提交给它的任何认证请求。
当用户提交凭据时,AbstractAuthenticationProcessingFilter 从 HttpServletRequest 创建一个 Authentication 以进行认证。创建的 Authentication 类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter 从 HttpServletRequest 中提交的 *用户名* 和 *密码* 创建一个 UsernamePasswordAuthenticationToken。
接下来,将 Authentication 传递给 AuthenticationManager 进行认证。
如果认证失败,则为 *失败*。
-
调用
RememberMeServices.loginFail。如果未配置记住我功能,则此操作为空操作。请参阅 rememberme 包。 -
调用
AuthenticationFailureHandler。请参阅AuthenticationFailureHandler接口。
如果认证成功,则为 *成功*。
-
SessionAuthenticationStrategy会收到新登录的通知。请参阅SessionAuthenticationStrategy接口。 -
加载 SecurityContextHolder 中任何已认证的
Authentication,并将其权限添加到返回的 Authentication 中。 -
将 Authentication 设置到 SecurityContextHolder 上。稍后,如果您需要保存
SecurityContext以便其可以自动设置到未来的请求中,则必须显式调用SecurityContextRepository#saveContext。请参阅SecurityContextHolderFilter类。 -
调用
RememberMeServices.loginSuccess。如果未配置记住我功能,则此操作为空操作。请参阅 rememberme 包。 -
ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent。 -
调用
AuthenticationSuccessHandler。请参阅AuthenticationSuccessHandler接口。