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 | 最后,我们在 SecurityContextHolder 上设置 SecurityContext 。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
从 SecurityContextHolder 获取 SecurityContext
。SecurityContext
包含一个 Authentication 对象。
身份验证
在 Spring Security 中,Authentication
接口主要有两个用途。
-
作为
AuthenticationManager
的输入,提供用户用于身份验证的凭据。在这种情况下使用时,isAuthenticated()
返回false
。 -
表示当前已验证的用户。您可以从 SecurityContext 中获取当前的
Authentication
。
Authentication
包含以下内容:
-
principal
:标识用户。当使用用户名/密码进行身份验证时,这通常是UserDetails
的实例。 -
credentials
:通常是密码。在许多情况下,这会在用户身份验证后被清除,以确保不会泄露。 -
authorities
:GrantedAuthority
实例是授予用户的顶级权限。两个例子是角色和范围。
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
会被控制器(即 Spring Security 的 Filters
实例)设置在 SecurityContextHolder 上,该控制器调用了 AuthenticationManager
。如果您没有与 Spring Security 的 Filters
实例集成,您可以直接设置 SecurityContextHolder
,并且不需要使用 AuthenticationManager
。
虽然 AuthenticationManager
的实现可以是任何东西,但最常见的实现是 ProviderManager
。
ProviderManager
ProviderManager
是 AuthenticationManager
最常用的实现。ProviderManager
会委托给一个 List
的 AuthenticationProvider
实例。每个 AuthenticationProvider
都可以有机会表明身份验证应该成功、失败,或者表明它无法做出决定并允许下游 AuthenticationProvider
做出决定。如果配置的 AuthenticationProvider
实例都无法进行身份验证,则身份验证将失败并抛出 ProviderNotFoundException
,这是一个特殊的 AuthenticationException
,表示 ProviderManager
未配置为支持传递给它的 Authentication
类型。
在实践中,每个 AuthenticationProvider
都知道如何执行特定类型的身份验证。例如,一个 AuthenticationProvider
可能能够验证用户名/密码,而另一个可能能够验证 SAML 断言。这使得每个 AuthenticationProvider
都可以执行非常具体的身份验证类型,同时支持多种身份验证类型,并只公开一个 AuthenticationManager
bean。
ProviderManager
还允许配置一个可选的父 AuthenticationManager
,如果没有任何 AuthenticationProvider
可以执行身份验证,则会咨询该父 AuthenticationManager
。父 AuthenticationManager
可以是任何类型的 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
接口。 -
将 Authentication 设置到 SecurityContextHolder 上。之后,如果您需要保存
SecurityContext
以便它可以在未来的请求中自动设置,则必须显式调用SecurityContextRepository#saveContext
。请参阅SecurityContextHolderFilter
类。 -
调用
RememberMeServices.loginSuccess
。如果未配置记住我,则此操作为无操作。请参阅rememberme
包。 -
ApplicationEventPublisher
发布InteractiveAuthenticationSuccessEvent
。 -
调用
AuthenticationSuccessHandler
。请参阅AuthenticationSuccessHandler
接口。