跳到主要内容
版本:7.0.2

Servlet 认证架构

DeepSeek V3 中英对照 Authentication Architecture Servlet Authentication Architecture

本讨论在Servlet安全:整体架构的基础上展开,旨在阐述Spring Security在Servlet身份验证中使用的主要架构组件。若需了解这些组件如何协同工作的具体流程,请参阅身份验证机制相关章节。

SecurityContextHolder

Spring Security认证模型的核心是SecurityContextHolder。它包含了SecurityContext

securitycontextholder

SecurityContextHolder 是 Spring Security 存储已认证用户详细信息的地方。Spring Security 并不关心 SecurityContextHolder 是如何被填充的。只要它包含一个值,该值就会被用作当前已认证的用户。

表明用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder

SecurityContext context = SecurityContextHolder.createEmptyContext(); 1
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); 2
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context); 3
  • 我们首先创建一个空的 SecurityContext。你应该创建一个新的 SecurityContext 实例,而不是使用 SecurityContextHolder.getContext().setAuthentication(authentication),以避免多个线程间的竞态条件。

  • 接下来,我们创建一个新的 Authentication 对象。Spring Security 并不关心在 SecurityContext 上设置什么类型的 Authentication 实现。这里,我们使用 TestingAuthenticationToken,因为它非常简单。一个更常见的生产场景是 UsernamePasswordAuthenticationToken(userDetails, password, authorities)

  • 最后,我们在 SecurityContextHolder 上设置 SecurityContext。Spring Security 使用此信息进行 授权

要获取已认证主体的信息,请访问 SecurityContextHolder

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

默认情况下,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

SecurityContext 是从 SecurityContextHolder 中获取的。SecurityContext 包含一个 Authentication 对象。

认证

Authentication 接口在 Spring Security 中主要有两个作用:

  • 作为 AuthenticationManager 的输入,用于提供用户为进行身份验证而提交的凭据。在此场景下使用时,isAuthenticated() 方法返回 false

  • 代表当前已通过身份验证的用户。你可以从 SecurityContext 中获取当前的 Authentication 对象。

Authentication 包含:

  • principal: 标识用户。使用用户名/密码进行身份验证时,这通常是 UserDetails 的一个实例。

  • credentials: 通常是密码。在许多情况下,用户通过身份验证后,此信息会被清除,以确保其不被泄露。

  • authorities: GrantedAuthority 实例是授予用户的高级权限。两个例子是角色和作用域。

它还配备了一个 AdditionalRequiredFactorsBuilder,允许您修改现有的 Authentication 实例,并可能将其与另一个实例合并。这在某些场景中非常有用,例如从一个认证步骤(如表单登录)中获取权限,并将其应用到另一个步骤(如一次性令牌登录),具体操作如下:

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();
}

GrantedAuthority

GrantedAuthority 实例是授予用户的高级权限。两个典型的例子是角色和作用域。

你可以通过 Authentication.getAuthorities() 方法获取 GrantedAuthority 实例。该方法会返回一个 GrantedAuthority 对象的 Collection 集合。顾名思义,GrantedAuthority 是授予给主体的权限。这些权限通常是“角色”,例如 ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。这些角色随后会被配置用于 Web 授权、方法授权和领域对象授权。Spring Security 的其他部分会解析这些权限并期望它们存在。当使用基于用户名/密码的身份验证时,GrantedAuthority 实例通常由 UserDetailsService 加载。

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

AuthenticationManager

AuthenticationManager 是定义 Spring Security 过滤器如何执行身份验证的 API。随后,调用 AuthenticationManager 的控制器(即 Spring Security 的过滤器实例)会将返回的 Authentication 设置到 SecurityContextHolder 上。如果你没有与 Spring Security 的 Filters 实例集成,可以直接设置 SecurityContextHolder,而不需要使用 AuthenticationManager

虽然 AuthenticationManager 的实现可以是任何形式,但最常见的实现是 ProviderManager

ProviderManager

ProviderManagerAuthenticationManager 最常用的实现。ProviderManager 将认证委托给一个 AuthenticationProvider 实例的 List。每个 AuthenticationProvider 都有机会指示认证应该成功、失败,或者表明它无法做出决定并允许下游的 AuthenticationProvider 来决定。如果配置的 AuthenticationProvider 实例都无法进行认证,则认证将失败并抛出 ProviderNotFoundException,这是一个特殊的 AuthenticationException,表明 ProviderManager 未配置为支持传入的 Authentication 类型。

providermanager

在实践中,每个 AuthenticationProvider 都知道如何执行特定类型的身份验证。例如,一个 AuthenticationProvider 可能能够验证用户名/密码,而另一个可能能够验证 SAML 断言。这使得每个 AuthenticationProvider 都能执行非常特定类型的身份验证,同时支持多种身份验证类型,并且只暴露一个 AuthenticationManager bean。

ProviderManager 还允许配置一个可选的父级 AuthenticationManager,当没有任何 AuthenticationProvider 能够执行身份验证时,便会咨询该父级管理器。父级管理器可以是任何类型的 AuthenticationManager,但它通常是 ProviderManager 的一个实例。

providermanager parent

实际上,多个 ProviderManager 实例可能共享同一个父级 AuthenticationManager。这在某些场景中相当常见,例如存在多个 SecurityFilterChain 实例,它们共享一些通用的认证逻辑(即共享的父级 AuthenticationManager),但同时也使用不同的认证机制(即不同的 ProviderManager 实例)。

providermanagers parent

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

备注

CredentialsContainer 接口在身份验证过程中扮演着关键角色。它允许在不再需要时清除凭证信息,从而通过确保敏感数据不会保留超过必要的时间来增强安全性。

在使用用户对象缓存时,例如为了提升无状态应用程序的性能,这可能会引发问题。如果 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

1 当用户提交凭据时,AbstractAuthenticationProcessingFilter 会基于待认证的 HttpServletRequest 创建一个 Authentication 对象。创建的 Authentication 类型取决于 AbstractAuthenticationProcessingFilter 的具体子类。例如,UsernamePasswordAuthenticationFilter 会根据 HttpServletRequest 中提交的用户名密码创建一个 UsernamePasswordAuthenticationToken

2 接下来,身份验证 被传递到 AuthenticationManager 进行认证。

3 如果身份验证失败,则 Failure

4 如果认证成功,则显示 成功

  • SessionAuthenticationStrategy 会收到新登录的通知。请参阅 SessionAuthenticationStrategy 接口。

  • SecurityContextHolder 中任何已认证的 Authentication 会被加载,其权限(authorities)会被添加到返回的 Authentication 中。

  • Authentication 被设置到 SecurityContextHolder 上。之后,如果需要保存 SecurityContext 以便在未来的请求中自动设置,必须显式调用 SecurityContextRepository#saveContext。请参阅 SecurityContextHolderFilter 类。

  • RememberMeServices.loginSuccess 被调用。如果未配置记住我(remember me)功能,此操作无效。请参阅 rememberme 包。

  • ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent 事件。

  • AuthenticationSuccessHandler 被调用。请参阅 AuthenticationSuccessHandler 接口。