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
从SecurityContextHolder获取SecurityContext
。SecurityContext
包含一个Authentication对象。
Authentication
Authentication
接口在 Spring Security 中主要服务于两个目的
-
作为
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
由控制器(即,由调用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
。父级可以是任何类型的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
接口。