Servlet 身份验证架构

本讨论扩展了 Servlet 安全:全局概览,描述了 Spring Security 在 Servlet 身份验证中使用的主要架构组件。如果您需要解释这些组件如何协同工作的具体流程,请查看 身份验证机制 相关部分。

SecurityContextHolder

Spring Security 身份验证模型的核心是 SecurityContextHolder。它包含 SecurityContext

securitycontextholder

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 策略。其他应用程序可能希望由安全线程生成的线程也采用相同的安全身份。您可以通过使用 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 来实现这一点。您可以从默认的 SecurityContextHolder.MODE_THREADLOCAL 更改模式,方法有两种。第一种是设置系统属性。第二种是在 SecurityContextHolder 上调用静态方法。大多数应用程序不需要从默认值更改。但是,如果您确实需要更改,请查看 SecurityContextHolder 的 JavaDoc 以了解更多信息。

SecurityContext

SecurityContextHolder 获取 SecurityContextSecurityContext 包含一个 Authentication 对象。

身份验证

在 Spring Security 中,Authentication 接口主要有两个用途。

  • 作为 AuthenticationManager 的输入,提供用户用于身份验证的凭据。在这种情况下使用时,isAuthenticated() 返回 false

  • 表示当前已验证的用户。您可以从 SecurityContext 中获取当前的 Authentication

Authentication 包含以下内容:

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

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

  • authoritiesGrantedAuthority 实例是授予用户的顶级权限。两个例子是角色和范围。

GrantedAuthority

GrantedAuthority 实例是授予用户的顶级权限。两个例子是角色和范围。

您可以从 Authentication.getAuthorities() 方法获取 GrantedAuthority 实例。此方法提供一个 GrantedAuthority 对象的 CollectionGrantedAuthority 是授予主体的权限。这些权限通常是“角色”,例如 ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。这些角色稍后将被配置用于 Web 授权、方法授权和域对象授权。Spring Security 的其他部分会解释这些权限并期望它们存在。当使用基于用户名/密码的身份验证时,GrantedAuthority 实例通常由 UserDetailsService 加载。

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

AuthenticationManager

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

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

ProviderManager

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

providermanager

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

ProviderManager 还允许配置一个可选的父 AuthenticationManager,如果没有任何 AuthenticationProvider 可以执行身份验证,则会咨询该父 AuthenticationManager。父 AuthenticationManager 可以是任何类型的 AuthenticationManager,但通常是 ProviderManager 的实例。

providermanager parent

事实上,多个 ProviderManager 实例可能共享同一个父 AuthenticationManager。这在有多个 SecurityFilterChain 实例具有某些共同的身份验证(共享的父 AuthenticationManager),但也有不同的身份验证机制(不同的 ProviderManager 实例)的情况下比较常见。

providermanagers parent

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

number 1 当用户提交其凭据时,AbstractAuthenticationProcessingFilter 会从 HttpServletRequest 中创建一个 Authentication 以进行身份验证。创建的 Authentication 类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter 会从 HttpServletRequest 中提交的用户名密码创建一个 UsernamePasswordAuthenticationToken

number 2 接下来,将 Authentication 传递到 AuthenticationManager 以进行身份验证。

number 3 如果身份验证失败,则为失败

number 4 如果身份验证成功,则为成功

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

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

  • 调用 RememberMeServices.loginSuccess。如果未配置记住我,则此操作为无操作。请参阅 rememberme 包。

  • ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent

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