Servlet 认证架构
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
本讨论对 Servlet Security 进行了扩展。架构图 阐述了 Spring Security 用于 Servlet 认证的主要架构组件。如果你需要具体的流程来解释这些部分是如何结合在一起的,请看 认证机制 的具体章节。
-
SecurityContextHolder -
SecurityContextHolder
是 Spring Security 存储 认证 用户细节的地方。 -
SecurityContext - 是从
SecurityContextHolder
获得的,包含了当前认证用户的Authentication
(认证)。 -
Authentication - 可以是
AuthenticationManager
的输入,以提供用户提供的认证凭证或来自SecurityContext
的当前用户。 -
GrantedAuthority - 在
Authentication
(认证)中授予委托人的一种权限(即role、scope等)。 -
AuthenticationManager - 定义 Spring Security 的 Filter 如何执行 认证 的API。
-
ProviderManager - 最常见的
AuthenticationManager
的实现。 -
AuthenticationProvider - 由
ProviderManager
用于执行特定类型的认证。 -
用
AuthenticationEntryPoint
请求凭证 - 用于从客户端请求凭证(即重定向到登录页面,发送WWW-Authenticate
响应,等等)。 -
AbstractAuthenticationProcessingFilter - 一个用于认证的基本
Filter
。这也让我们很好地了解了认证的高层流程以及各部分是如何协作的。
SecurityContextHolder
Spring Security 的认证模型的核心是 SecurityContextHolder
。它包含了SecurityContext。
![securitycontextholder](../../_images/servlet/authentication/architecture/securitycontextholder.png)
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
策略。其他应用程序可能想让安全线程所产生的线程也承担相同的 security 身份。你可以通过使用 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
实例是用户被授予的高级权限。两个例子是角色(role)和作用域(scope)。
GrantedAuthority
GrantedAuthority
实例是用户被授予的高级权限。两个例子是角色(role)和作用域(scope)。
你可以从Authentication.getAuthorities()
法中获得 GrantedAuthority
实例。这个方法提供了一个 GrantedAuthority
对象的集合。毫不奇怪,GrantedAuthority
是授予委托人的一种权限。这种授权通常是 “roles”,例如 ROLE_ADMINISTRATOR
或 ROLE_HR_SUPERVISOR
。这些角色后来被配置为Web授权、方法授权和域对象授权。Spring Security 的其他部分会解释这些授权并期望它们存在。当使用基于用户名/密码的认证时, GrantedAuthority
实例通常由 UserDetailsService
加载。
通常,GrantedAuthority
对象是应用范围的权限。它们并不特定于某个特定的域对象。因此,你不可能有一个 GrantedAuthority
来代表第54号 Employee
对象的权限,因为如果有成千上万个这样的授权,你会很快耗尽内存(或者,至少会导致应用程序花很长时间来验证用户)。当然, Spring Security 被明确设计为处理这种常见的需求,但你应该转而使用项目的 domain object security 功能来达到这个目的。
AuthenticationManager
AuthenticationManager
是定义 Spring Security 的 Filter 如何执行 认证 的API。返回的 认证是由调用 AuthenticationManager
的控制器(即 Spring Security的 Filter
实例)在 SecurityContextHolder 上设置的。如果你不与 Spring Security 的 Filter 实例集成,你可以直接设置 SecurityContextHolder
,不需要使用 AuthenticationManager
。
虽然 AuthenticationManager
的实现可以是任何东西,但最常见的实现是ProviderManager
。
ProviderManager
ProviderManager
是最常用的AuthenticationManager
的实现。ProviderManager
委托给一个 List
AuthenticationProvider
实例。每个 AuthenticationProvider
都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的 AuthenticationProvider
来决定。如果配置的 AuthenticationProvider
实例中没有一个能进行认证,那么认证就会以 ProviderNotFoundException
而失败,这是一个特殊的 AuthenticationException
,表明 ProviderManager
没有被配置为支持被传入它的 Authentication
类型。
![providermanager](../../_images/servlet/authentication/architecture/providermanager.png)
在实践中,每个 AuthenticationProvider
都知道如何执行特定类型的认证。例如,一个 AuthenticationProvider
可能能够验证一个用户名/密码,而另一个可能能够验证一个 SAML 断言。这让每个 AuthenticationProvider
在支持多种类型的认证的同时,可以做一种非常具体的认证类型,并且只暴露一个 AuthenticationManager
Bean。
ProviderManager
还允许配置一个可选的父级 AuthenticationManager
,在没有 AuthenticationProvider
可以执行认证的情况下,可以参考它。父级可以是任何类型的 AuthenticationManager
,但它通常是 ProviderManager
的一个实例。
![providermanager parent](../../_images/servlet/authentication/architecture/providermanager-parent.png)
事实上,多个 ProviderManager
实例可能共享同一个父级 AuthenticationManager
。这在有多个 SecurityFilterChain
实例的场景中有些常见,这些实例有一些共同的认证(共享的父 AuthenticationManager
),但也有不同的认证机制(不同的 ProviderManager
实例)。
![providermanagers parent](../../_images/servlet/authentication/architecture/providermanagers-parent.png)
默认情况下,ProviderManager
会尝试从 Authentication
对象中清除任何敏感的凭证信息,该对象由成功的认证请求返回。这可以防止密码等信息在 HttpSession
中保留超过必要的时间。
当你使用用户对象的缓存时,这可能会导致问题,例如,在一个无状态的应用程序中提高性能。如果 Authentication
包含对缓存中的一个对象的引用(比如 UserDetails
实例),而这个对象的凭证已经被删除,那么就不可能再针对缓存的值进行认证。如果你使用一个缓存,你需要考虑到这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的 Authentication
对象的 AuthenticationProvider
中制作一个对象的副本。另外,你可以禁用 ProviderManager
上的 eraseCredentialsAfterAuthentication
属性。参见 ProviderManager 类的Javadoc。
AuthenticationProvider
你可以在 ProviderManager
中注入多个 AuthenticationProvider
实例。每个 AuthenticationProvider
都执行一种特定类型的认证。例如, DaoAuthenticationProvider
支持基于用户名/密码的认证,而 JwtAuthenticationProvider
支持认证JWT令牌。
用 AuthenticationEntryPoint
请求凭证
AuthenticationEntryPoint
用于发送一个要求客户端提供凭证的HTTP响应。
有时,客户端会主动包含凭证(如用户名和密码)来请求资源。在这些情况下,Spring Security 不需要提供要求客户端提供凭证的HTTP响应,因为这些凭证已经被包括在内。
在其他情况下,客户端向他们未被授权访问的资源发出未经认证的请求。在这种情况下, AuthenticationEntryPoint
的实现被用来请求客户端的凭证。 AuthenticationEntryPoint
的实现可能会执行 重定向到一个登录页面,用 WWW-Authenticate 头来响应,或采取其他行动。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
被用作验证用户凭证的基础 Filter
。在认证凭证之前,Spring Security 通常通过使用AuthenticationEntryPoint
来请求凭证。
接下来,AbstractAuthenticationProcessingFilter
可以对提交给它的任何认证请求进行认证。
![abstractauthenticationprocessingfilter](../../_images/servlet/authentication/architecture/abstractauthenticationprocessingfilter.png)
当用户提交他们的凭证时,
AbstractAuthenticationProcessingFilter
会从 HttpServletRequest
中创建一个要认证的Authentication
。创建的认证的类型取决于 AbstractAuthenticationProcessingFilter
的子类。例如,UsernamePasswordAuthenticationFilter
从 HttpServletRequest
中提交的 username 和 password 创建一个 UsernamePasswordAuthenticationToken
。
接下来,
Authentication
被传入 AuthenticationManager
,以进行认证。
如果认证失败,则为 Failure。
-
RememberMeServices.loginFail
被调用。如果没有配置记住我(remember me),这就是一个无用功。请参阅rememberme
包。 -
AuthenticationFailureHandler
被调用。参见AuthenticationFailureHandler
接口。
如果认证成功,则为 Success。
-
SessionAuthenticationStrategy
被通知有新的登录。参见SessionAuthenticationStrategy
接口。 -
Authentication 是在 SecurityContextHolder 上设置的。后来,如果你需要保存
SecurityContext
以便在未来的请求中自动设置,必须显式调用SecurityContextRepository#saveContext
。参见SecurityContextHolderFilter
类。 -
RememberMeServices.loginSuccess
被调用。如果没有配置 remember me,这就是一个无用功。请参阅rememberme
包。 -
ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent
事件。 -
AuthenticationSuccessHandler
被调用。参见AuthenticationSuccessHandler
接口。