Servlet 认证架构
本讨论在Servlet 安全:总体概述的基础上,进一步描述了 Spring Security 在 Servlet 认证中使用的主要架构组件。如果你需要具体的流程来解释这些组件如何协同工作,请参阅认证机制特定部分。
-
SecurityContextHolder -
SecurityContextHolder
是 Spring Security 存储谁已认证的详细信息的地方。 -
SecurityContext - 从
SecurityContextHolder
中获取,包含当前已认证用户的Authentication
。 -
Authentication - 可以作为
AuthenticationManager
的输入,提供用户用于认证的凭证,或者从SecurityContext
中获取当前用户。 -
GrantedAuthority - 授予
Authentication
上主体的权限(即角色、范围等)。 -
AuthenticationManager - 定义 Spring Security 的过滤器如何执行认证的 API。
-
ProviderManager -
AuthenticationManager
最常见的实现。 -
AuthenticationProvider - 由
ProviderManager
使用,执行特定类型的认证。 -
使用 AuthenticationEntryPoint 请求凭证 - 用于向客户端请求凭证(例如重定向到登录页面、发送
WWW-Authenticate
响应等)。 -
AbstractAuthenticationProcessingFilter - 用于认证的基础
Filter
。这也可以很好地了解认证的高层流程以及各部分如何协同工作。
SecurityContextHolder
在 Spring Security 的认证模型的核心是 SecurityContextHolder
。它包含了 SecurityContext。
SecurityContextHolder
是 Spring Security 存储已认证用户详细信息的地方。Spring Security 并不关心 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
我们首先创建一个空的
SecurityContext
。你应该创建一个新的SecurityContext
实例,而不是使用SecurityContextHolder.getContext().setAuthentication(authentication)
,以避免在多个线程之间出现竞争条件。接下来,我们创建一个新的 Authentication 对象。Spring Security 不关心设置在
SecurityContext
上的Authentication
实现类型。这里我们使用TestingAuthenticationToken
,因为它非常简单。更常见的生产场景是UsernamePasswordAuthenticationToken(userDetails, password, authorities)
。最后,我们将
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 虚拟机中的所有线程都使用相同的 security context。您可以在启动时配置 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 实例是授予用户的高级权限。两个例子是角色和范围。
GrantedAuthority
GrantedAuthority 实例是授予用户的高级权限。两个例子是角色和范围。
你可以从 Authentication.getAuthorities() 方法中获取 GrantedAuthority
实例。此方法提供了一个 GrantedAuthority
对象的 Collection
。不出所料,GrantedAuthority
是授予主体的权限。这些权限通常是“角色”,例如 ROLE_ADMINISTRATOR
或 ROLE_HR_SUPERVISOR
。这些角色稍后将被配置用于 web 授权、方法授权和领域对象授权。Spring Security 的其他部分会解释这些权限并期望它们存在。在使用基于用户名/密码的认证时,GrantedAuthority
实例通常由 UserDetailsService 加载。
通常,GrantedAuthority
对象是应用程序范围的权限。它们并不特定于某个给定的领域对象。因此,你不太可能有一个 GrantedAuthority
来表示对编号为 54 的 Employee
对象的权限,因为如果有成千上万这样的权限,你很快就会耗尽内存(或者,至少会导致应用程序在验证用户身份时花费很长时间)。当然,Spring Security 是专门设计来处理这种常见需求的,但你应该为此目的使用项目的领域对象安全功能。
AuthenticationManager
AuthenticationManager 是定义 Spring Security 的 Filters 如何执行认证的 API。返回的 认证 由调用 AuthenticationManager
的控制器(即,Spring Security 的 Filters 实例)设置到 SecurityContextHolder 中。如果你不与 Spring Security 的 Filters
实例集成,可以直接设置 SecurityContextHolder
,而不必使用 AuthenticationManager
。
虽然 AuthenticationManager
的实现可以是任意的,但最常见的实现是 ProviderManager。
ProviderManager
ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager
委托给一个 AuthenticationProvider 实例的 List
。每个 AuthenticationProvider
都有机会表明认证应该成功、失败,或者表明它无法做出决定并允许下游的 AuthenticationProvider
来决定。如果配置的所有 AuthenticationProvider
实例都无法进行认证,则认证会因 ProviderNotFoundException
而失败,这是一个特殊的 AuthenticationException
,表示 ProviderManager
未配置为支持传递给它的 Authentication
类型。
实际上,每个 AuthenticationProvider
都知道如何执行特定类型的认证。例如,一个 AuthenticationProvider
可能能够验证用户名/密码,而另一个则可能能够认证 SAML 断言。这样,每个 AuthenticationProvider
只需处理一种非常具体的认证类型,同时支持多种认证类型,并且只暴露一个 AuthenticationManager
bean。
ProviderManager
还允许配置一个可选的父 AuthenticationManager
,在没有任何 AuthenticationProvider
能够执行认证时会咨询该父 AuthenticationManager
。父 AuthenticationManager
可以是任何类型的 AuthenticationManager
,但它通常是一个 ProviderManager
的实例。
实际上,多个 ProviderManager
实例可能会共享同一个父 AuthenticationManager
。在有多个 SecurityFilterChain 实例的情况下,这种情况比较常见,这些实例有一些共同的认证(共享的父 AuthenticationManager
),但也存在不同的认证机制(不同的 ProviderManager
实例)。
默认情况下,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
可以认证提交给它的任何认证请求。
1 当用户提交其凭证时,AbstractAuthenticationProcessingFilter
会从 HttpServletRequest
创建一个Authentication 以进行认证。创建的 Authentication
类型取决于 AbstractAuthenticationProcessingFilter
的子类。例如,UsernamePasswordAuthenticationFilter 会根据 HttpServletRequest
中提交的 用户名 和 密码 创建一个 UsernamePasswordAuthenticationToken
。
2 接下来,Authentication 被传递到 AuthenticationManager 进行认证。
3 如果身份验证失败,则为Failure。
-
调用
RememberMeServices.loginFail
。如果未配置记住我功能,则此操作无效。请参阅 rememberme 包。 -
调用
AuthenticationFailureHandler
。请参阅 AuthenticationFailureHandler 接口。
4 如果身份验证成功,则 Success。
-
SessionAuthenticationStrategy
会收到新登录的通知。请参阅 SessionAuthenticationStrategy 接口。 -
Authentication 会被设置到 SecurityContextHolder 中。稍后,如果您需要保存
SecurityContext
以便在将来的请求中自动设置,则必须显式调用SecurityContextRepository#saveContext
。请参阅 SecurityContextHolderFilter 类。 -
调用
RememberMeServices.loginSuccess
。如果未配置记住我功能,这将是一个空操作。请参阅 rememberme 包。 -
ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent
事件。 -
调用
AuthenticationSuccessHandler
。请参阅 AuthenticationSuccessHandler 接口。