Servlet 身份验证架构

本文档基于 Servlet 安全:全局视图 的基础上,扩展描述了 Spring Security 在 Servlet 身份验证中使用的主要架构组件。如果你需要具体的流程来解释这些组件如何协同工作,请查看 身份验证机制 特定章节。

  • SecurityContextHolder - SecurityContextHolder 是 Spring Security 存储已认证主体详细信息的地方。

  • SecurityContext - 从 SecurityContextHolder 获取,包含当前已认证用户的 Authentication

  • Authentication - 可以作为 AuthenticationManager 的输入,提供用户用于认证的凭证;也可以是 SecurityContext 中当前用户的信息。

  • GrantedAuthority - 授予给 Authentication 上的 principal 的权限(例如角色、作用域等)

  • AuthenticationManager - 定义 Spring Security 的 Filter 如何执行 身份验证 的 API。

  • ProviderManager - 是最常用的 AuthenticationManager 实现。

  • AuthenticationProvider - 由 ProviderManager 使用,用于执行特定类型的身份验证。

  • 使用 AuthenticationEntryPoint 请求凭证 - 用于向客户端请求凭证(例如重定向到登录页、发送 WWW-Authenticate 响应等)

  • AbstractAuthenticationProcessingFilter - 一个基础的 Filter,用于身份验证。这也很好地展示了身份验证的高级流程以及各部分如何协同工作。

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

SecurityContextSecurityContextHolder 获取。SecurityContext 包含一个 Authentication 对象。

Authentication

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

  • 作为 AuthenticationManager 的输入,提供用户用于认证的凭证。在这种情况下,isAuthenticated() 返回 false.

  • 代表当前已认证的用户。你可以从 SecurityContext 获取当前 Authentication.

Authentication 包含:

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

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

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

GrantedAuthority

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

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

通常,GrantedAuthority 对象是应用程序范围内的权限。它们不特定于某个领域对象。因此,你不太可能会有一个 GrantedAuthority 来表示对员工对象编号 54 的权限,因为如果这样的权限有数千个,你很快就会耗尽内存(或者至少会导致应用程序认证用户所需时间过长)。当然,Spring Security 明确设计了处理这一常见需求的功能,但你应该转而使用该项目的领域对象安全能力来实现此目的。

AuthenticationManager

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

虽然 AuthenticationManager 的实现可以是任何类型,但最常见的实现是 ProviderManager.

ProviderManager

ProviderManagerAuthenticationManager 最常用的实现。ProviderManager 委托给一个 AuthenticationProvider 列表。每个 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

你可以将多个 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 当用户提交凭证时,AbstractAuthenticationProcessingFilterHttpServletRequest 创建一个 Authentication 对象进行认证。创建的 Authentication 类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilterHttpServletRequest 中提交的 *username* 和 *password* 创建一个 UsernamePasswordAuthenticationToken.

number 2 接下来,Authentication 被传递到 AuthenticationManager 中进行认证。

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

  • SecurityContextHolder 被清除。

  • RememberMeServices.loginFail 被调用。如果没有配置 remember me,则这是一个无操作。请参阅 rememberme 包。

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

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

  • SessionAuthenticationStrategy 通知新登录。请参阅 SessionAuthenticationStrategy 接口。

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

  • RememberMeServices.loginSuccess 被调用。如果没有配置 remember me,则这是一个无操作。请参阅 rememberme 包。

  • ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent.

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