Servlet 认证架构

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

本讨论对 Servlet Security 进行了扩展。架构图 阐述了 Spring Security 用于 Servlet 认证的主要架构组件。如果你需要具体的流程来解释这些部分是如何结合在一起的,请看 认证机制 的具体章节。

SecurityContextHolder

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

securitycontextholder

SecurityContextHolder 是 Spring Security 存储用户 验证 细节的地方。Spring Security 并不关心 SecurityContextHolder 是如何被填充的。如果它包含一个值,它就被用作当前认证的用户。

最简单的方法是直接设置 SecurityContextHolder 来表明用户已被认证。

Example 1. Setting 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

Access Currently Authenticated User
  • 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 策略。其他应用程序可能想让安全线程所产生的线程也承担相同的 security 身份。你可以通过使用 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 来实现。你可以通过两种方式改变默认的 SecurityContextHolder.MODE_THREADLOCAL 的模式。第一个是设置一个系统属性。第二种是调用 SecurityContextHolder 的一个静态方法。大多数应用程序不需要改变默认值。但是,如果你需要,请看一下 SecurityContextHolder 的 JavaDoc 以了解更多。

SecurityContext

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

Authentication

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

  • AuthenticationManager 的一个输入,用于提供用户为验证而提供的凭证。当在这种情况下使用时,isAuthenticated() 返回 false

  • 代表当前认证的用户。你可以从 SecurityContext 中获得当前的 Authentication

认证(Authentication)包含了:

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

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

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

GrantedAuthority

GrantedAuthority 实例是用户被授予的高级权限。两个例子是角色(role)和作用域(scope)。

你可以从Authentication.getAuthorities()法中获得 GrantedAuthority 实例。这个方法提供了一个 GrantedAuthority 对象的集合。毫不奇怪,GrantedAuthority 是授予委托人的一种权限。这种授权通常是 “roles”,例如 ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。这些角色后来被配置为Web授权、方法授权和域对象授权。Spring Security 的其他部分会解释这些授权并期望它们存在。当使用基于用户名/密码的认证时, GrantedAuthority 实例通常由 UserDetailsService 加载。

通常,GrantedAuthority 对象是应用范围的权限。它们并不特定于某个特定的域对象。因此,你不可能有一个 GrantedAuthority 来代表第54号 Employee 对象的权限,因为如果有成千上万个这样的授权,你会很快耗尽内存(或者,至少会导致应用程序花很长时间来验证用户)。当然, Spring Security 被明确设计为处理这种常见的需求,但你应该转而使用项目的 domain object security 功能来达到这个目的。

AuthenticationManager

AuthenticationManager 是定义 Spring Security 的 Filter 如何执行 认证 的API。返回的 认证是由调用 AuthenticationManager 的控制器(即 Spring Security的 Filter 实例)在 SecurityContextHolder 上设置的。如果你不与 Spring Security 的 Filter 实例集成,你可以直接设置 SecurityContextHolder,不需要使用 AuthenticationManager

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

ProviderManager

ProviderManager是最常用的AuthenticationManager的实现。ProviderManager 委托给一个 ListAuthenticationProvider实例。每个 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 中保留超过必要的时间。

当你使用用户对象的缓存时,这可能会导致问题,例如,在一个无状态的应用程序中提高性能。如果 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

number 1 当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter 会从 HttpServletRequest 中创建一个要认证的Authentication。创建的认证的类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilterHttpServletRequest 中提交的 usernamepassword 创建一个 UsernamePasswordAuthenticationToken

number 2 接下来,Authentication 被传入 AuthenticationManager,以进行认证。

number 3 如果认证失败,则为 Failure

number 4 如果认证成功,则为 Success

  • SessionAuthenticationStrategy 被通知有新的登录。参见 SessionAuthenticationStrategy 接口。

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

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

  • ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent 事件。

  • AuthenticationSuccessHandler 被调用。参见 AuthenticationSuccessHandler 接口。