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 最后,我们将SecurityContext设置到SecurityContextHolder上。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对象。

Authentication

Authentication接口在 Spring Security 中主要服务于两个目的

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

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

Authentication包含

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

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

  • authoritiesGrantedAuthority实例是授予用户的较高权限。两个示例是角色和范围。

GrantedAuthority

GrantedAuthority实例是授予用户的较高权限。两个示例是角色和范围。

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

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

AuthenticationManager

AuthenticationManager是定义 Spring Security 的过滤器如何执行身份验证的 API。然后,返回的Authentication由控制器(即,由调用AuthenticationManagerSpring Security 的Filters实例)设置到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。父级可以是任何类型的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 接口。