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
是 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
类型。
在实践中,每个 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
你可以在 ProviderManager
中注入多个 AuthenticationProvider
实例。每个 AuthenticationProvider
都执行一种特定类型的认证。例如, DaoAuthenticationProvider
支持基于用户名/密码的认证,而 JwtAuthenticationProvider
支持认证JWT令牌。
用 AuthenticationEntryPoint
请求凭证
AuthenticationEntryPoint
用于发送一个要求客户端提供凭证的HTTP响应。
有时,客户端会主动包含凭证(如用户名和密码)来请求资源。在这些情况下,Spring Security 不需要提供要求客户端提供凭证的HTTP响应,因为这些凭证已经被包括在内。
在其他情况下,客户端向他们未被授权访问的资源发出未经认证的请求。在这种情况下, AuthenticationEntryPoint
的实现被用来请求客户端的凭证。 AuthenticationEntryPoint
的实现可能会执行 重定向到一个登录页面,用 WWW-Authenticate 头来响应,或采取其他行动。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
被用作验证用户凭证的基础 Filter
。在认证凭证之前,Spring Security 通常通过使用AuthenticationEntryPoint
来请求凭证。
接下来,AbstractAuthenticationProcessingFilter
可以对提交给它的任何认证请求进行认证。
当用户提交他们的凭证时,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
接口。