Servlet 认证架构

本讨论在Servlet 安全:整体概览的基础上进一步展开,描述了 Spring Security 在 Servlet 认证中使用的主要架构组件。 如果您需要了解这些组件如何协同工作的具体流程,请参阅认证机制的各个具体章节。spring-doc.cadn.net.cn

安全上下文持有者

Spring Security 认证模型的核心是 SecurityContextHolder。 它包含 SecurityContextspring-doc.cadn.net.cn

securitycontextholder

SecurityContextHolder 是 Spring Security 存储当前已认证用户详细信息的地方。 Spring Security 并不关心 SecurityContextHolder 是如何被填充的。 只要其中包含一个值,就会将其用作当前已认证的用户。spring-doc.cadn.net.cn

表明用户已认证的最简单方法是直接设置 SecurityContextHolderspring-doc.cadn.net.cn

设置 SecurityContextHolder
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 使用此信息进行授权

要获取有关已认证主体的信息,请访问SecurityContextHolderspring-doc.cadn.net.cn

访问当前已认证的用户
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

在 Spring MVC 中,您可以使用 @AuthenticationPrincipal 解析当前主体,并使用 @CurrentSecurityContext 解析完整的SecurityContext。 如需访问 Servlet API,请使用 HttpServletRequest#getRemoteUserspring-doc.cadn.net.cn

默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些详细信息,这意味着即使没有显式地将 SecurityContext 作为参数传递给同一线程中的方法,这些方法也始终可以访问到 SecurityContext。 如果在当前主体(principal)的请求处理完成后注意清理线程,以这种方式使用 ThreadLocal 是相当安全的。 Spring Security 的 FilterChainProxy 会确保 SecurityContext 始终被清除。spring-doc.cadn.net.cn

一些应用程序并不完全适合使用ThreadLocal,因为它们处理线程的方式具有特定性。 例如,一个Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。 您可以在启动时配置SecurityContextHolder,并指定策略以说明您希望如何存储此上下文。 对于独立应用程序,您可以使用SecurityContextHolder.MODE_GLOBAL策略。 其他应用程序可能希望由安全线程生成的线程也假设相同的安全身份。 通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL可以实现这一点。 您可以通过两种方式更改模式: 第一种是设置系统属性; 第二种是在SecurityContextHolder上调用静态方法。 大多数应用程序不需要从默认值更改。 然而,如果您确实需要这样做,请查阅SecurityContextHolder的JavaDoc以获取更多信息。spring-doc.cadn.net.cn

安全上下文

The SecurityContext 是从 SecurityContextHolder 获取的。 The SecurityContext 包含一个 Authentication 对象。spring-doc.cadn.net.cn

身份验证

The Authentication 接口在 Spring Security 中主要有两个用途:spring-doc.cadn.net.cn

Authentication 包含:spring-doc.cadn.net.cn

它还配备了一个 AdditionalRequiredFactorsBuilder,允许你修改现有的 Authentication 实例,并可能将其与另一个实例合并。 这在某些场景下非常有用,例如从一个认证步骤(如表单登录)中获取权限信息,并将其应用到另一个认证步骤(如一次性Tokens登录)中,如下所示:spring-doc.cadn.net.cn

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 是授予用户的高级权限。 两个示例是角色和范围。spring-doc.cadn.net.cn

您可以从 Authentication.getAuthorities() 方法获取 GrantedAuthority 实例。 此方法提供 GrantedAuthority 对象的 Collection。 毫不奇怪,GrantedAuthority 是授予主体的一种权限。 此类权限通常是“角色”,例如 ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。 这些角色随后被配置用于 Web 授权、方法授权和领域对象授权。 Spring Security 的其他部分会解释这些权限并期望它们存在。 在使用基于用户名/密码的认证时,GrantedAuthority 实例通常由 UserDetailsService 加载。spring-doc.cadn.net.cn

通常,GrantedAuthority 对象表示的是应用程序范围内的权限。 它们并不针对某个特定的领域对象。 因此,你不太可能拥有一个 GrantedAuthority 来表示对编号为 54 的 Employee 对象的权限, 因为如果存在成千上万个这样的权限,你很快就会耗尽内存(或者至少会导致应用程序在认证用户时花费很长时间)。 当然,Spring Security 明确设计用于处理这种常见需求,但你应该改用该项目的领域对象安全功能来实现这一目的。spring-doc.cadn.net.cn

认证管理器

AuthenticationManager 是定义 Spring Security 的过滤器如何执行 认证 的 API。 返回的 Authentication 随后由调用 AuthenticationManager 的控制器(即由 Spring Security 的 Filters 实例)设置到 SecurityContextHolder 中。 如果您未与 Spring Security 的 Filters 实例集成,您可以直接设置 SecurityContextHolder,且无需使用 AuthenticationManagerspring-doc.cadn.net.cn

尽管AuthenticationManager的实现可以是任意的,但最常见的实现是ProviderManagerspring-doc.cadn.net.cn

提供者管理器

ProviderManagerAuthenticationManager 最常用的实现。 ProviderManager 委托给 AuthenticationProvider 实例的 List。 每个 AuthenticationProvider 都有机会表明认证应成功、失败,或表示其无法做出决定并允许下游 AuthenticationProvider 进行决策。 如果配置的任何 AuthenticationProvider 实例都无法完成认证,则认证将失败并抛出 ProviderNotFoundException,这是一种特殊的 AuthenticationException,用于指示 ProviderManager 未配置为支持传入的 Authentication 类型。spring-doc.cadn.net.cn

providermanager

在实践中,每个 AuthenticationProvider 都知道如何执行一种特定类型的认证。 例如,一个 AuthenticationProvider 可能能够验证用户名/密码,而另一个则可能能够认证 SAML 断言。 这使得每个 AuthenticationProvider 都可以专注于执行非常具体的认证类型,同时支持多种认证方式,并仅对外暴露一个 AuthenticationManager Bean。spring-doc.cadn.net.cn

ProviderManager 还允许配置一个可选的父级 AuthenticationManager,当没有 AuthenticationProvider 能够执行身份验证时,会转而咨询该父级。 该父级可以是任意类型的 AuthenticationManager,但它通常是一个 ProviderManager 的实例。spring-doc.cadn.net.cn

providermanager parent

实际上,多个 ProviderManager 实例可能共享同一个父级 AuthenticationManager。 这在以下场景中较为常见:存在多个具有某些共同认证信息的 SecurityFilterChain 实例(即共享的父级 AuthenticationManager),但也拥有不同的认证机制(即不同的 ProviderManager 实例)。spring-doc.cadn.net.cn

providermanagers parent

默认情况下,ProviderManager 会尝试清除由成功身份验证请求返回的 Authentication 对象中的任何敏感凭据信息。 这可以防止密码等信息在 HttpSession 中保留超过必要的时间。spring-doc.cadn.net.cn

CredentialsContainer 接口在身份验证过程中起着关键作用。 它允许在凭证信息不再需要时将其清除,从而通过确保敏感数据不会被保留超过必要的时间来增强安全性。spring-doc.cadn.net.cn

这可能会在使用用户对象缓存时引发问题,例如在无状态应用中用于提升性能。 如果 Authentication 包含对缓存中对象的引用(例如 UserDetails 实例),且该对象的凭据已被移除,则无法再使用缓存的值进行身份验证。 如果您使用缓存,请务必考虑这一点。 一个明显的解决方案是首先复制该对象,可以在缓存实现中完成,也可以在创建返回的 Authentication 对象的 AuthenticationProvider 中完成。 或者,您可以禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。 请参阅 ProviderManager 类的 Javadoc。spring-doc.cadn.net.cn

认证提供者

您可以将多个 实例 注入到 认证管理器 中。 每个 认证提供者 执行特定类型的身份验证。 例如,用户名密码认证提供者 支持基于用户名/密码的身份验证,而 JWT Tokens认证提供者 支持 JWT Tokens的身份验证。spring-doc.cadn.net.cn

请求凭据与AuthenticationEntryPoint

AuthenticationEntryPoint 用于发送请求客户端提供凭据的 HTTP 响应。spring-doc.cadn.net.cn

有时,客户端会主动在请求资源时包含凭据(例如用户名和密码)。 在这种情况下,Spring Security 无需再向客户端返回一个要求提供凭据的 HTTP 响应,因为凭据已经包含在请求中了。spring-doc.cadn.net.cn

在其他情况下,客户端会向其未被授权访问的资源发起未经身份验证的请求。 此时,将使用 AuthenticationEntryPoint 的一个实现来向客户端请求凭据。 AuthenticationEntryPoint 的实现可能会执行重定向到登录页面、返回一个WWW-Authenticate 响应头,或采取其他操作。spring-doc.cadn.net.cn

抽象认证处理过滤器

AbstractAuthenticationProcessingFilter 被用作对用户凭据进行身份验证的基础 Filter。 在凭据可以被身份验证之前,Spring Security 通常使用 AuthenticationEntryPoint 请求凭据。spring-doc.cadn.net.cn

接下来,AbstractAuthenticationProcessingFilter 可以认证提交给它的任何身份验证请求。spring-doc.cadn.net.cn

abstractauthenticationprocessingfilter

number 1当用户提交凭据时,AbstractAuthenticationProcessingFilter创建Authentication来自HttpServletRequest需要进行身份验证。 类型Authentication创建取决于子类AbstractAuthenticationProcessingFilter. 例如,UsernamePasswordAuthenticationFilter创建UsernamePasswordAuthenticationToken来自一个用户名密码在提交的HttpServletRequest.spring-doc.cadn.net.cn

number 2接下来,Authentication被传递到AuthenticationManager需要身份验证。spring-doc.cadn.net.cn

number 3如果身份验证失败,则失败.spring-doc.cadn.net.cn

number 4如果认证成功,则成功.spring-doc.cadn.net.cn