SecurityContextHolder
Spring Security 认证模型的核心是SecurityContextHolder
。它包含 SecurityContext 。
SecurityContextHolder
是 Spring Security 存储身份验证人员详细信息的地方。 Spring Security 不关心如何填充SecurityContextHolder
。如果它不为空则将其用作当前经过身份验证的用户。
指示用户已通过身份验证的最简单方法是直接设置SecurityContextHolder
。
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 1Authentication authentication =new TestingAuthenticationToken("username", "password", "ROLE_USER"); // 2context.setAuthentication(authentication);SecurityContextHolder.setContext(context); // 3
首先创建一个空的 SecurityContext 。创建新的 SecurityContext 实例而不是使用 SecurityContextHolder.getContext().setAuthentication(authentication) 以避免跨多个线程的竞争条件很重要。
接下来创建一个新的身份验证对象。 Spring Security 不关心在 SecurityContext 上设置了什么类型的身份验证实现。这里使用TestingAuthenticationToken
因为它非常简单。更常见的生产场景是UsernamePasswordAuthenticationToken
(用户详细信息、密码、权限)。
最后在SecurityContextHolder
上设置 SecurityContext 。 Spring Security 将使用此信息进行授权。
如果想要获得有关经过身份验证的主体的信息,可以通过访问SecurityContextHolder
来实现。
SecurityContext context = SecurityContextHolder.getContext();Authentication authentication = context.getAuthentication();String username = authentication.getName();Object principal = authentication.getPrincipal();Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
默认情况下,SecurityContextHolder
使用 ThreadLocal 来存储这些详细信息,这意味着 SecurityContext 始终可用于同一线程中的方法,即使 SecurityContext 没有作为这些方法的参数显式传递。如果在处理当前主体的请求后小心地清除线程,那么以这种方式使用 ThreadLocal 是非常安全的。 Spring Security 的FilterChainProxy
可以确保始终清除 SecurityContext 。
有些应用程序并不完全适合使用 ThreadLocal ,因为它们使用线程的方式很特殊。例如, Swing 客户端可能希望 Java 虚拟机中的所有线程使用相同的安全上下文。SecurityContextHolder
可以在启动时配置策略,以指定如何存储上下文。对于独立应用程序,将使用SecurityContextHolder.MODE_GLOBAL
。其他应用程序可能希望安全线程生成的线程也具有相同的安全标识可以使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
。有两种方式可以从默认的SecurityContextHolder.MODE_THREADLOCAL
改变模式。第一个是设置系统属性,第二个是调用SecurityContextHolder
上的静态方法。
public static void setStrategyName(String strategyName) {SecurityContextHolder.strategyName = strategyName;initialize();}
Authentication
在 Spring Security 中,身份验证有两个主要目的:
AuthenticationManager 的输入,用于提供用户提供的用于身份验证的凭据。在此场景中使用时, isAuthenticated() 返回false。
表示当前经过身份验证的用户。当前身份验证可以从 SecurityContext 获得。
身份验证包含:
主体 - 标识用户。当使用 用户名/密码 进行身份验证时,这通常是 UserDetails 的一个实例。
凭据 - 通常是密码。在许多情况下,这将在用户经过身份验证后清除,以确保不会泄漏。
权限 -GrantedAuthority
是授予用户的高级权限。
GrantedAuthority
GrantedAuthority
是授予用户的高级权限。
GrantedAuthority
可以从Authentication.getAuthorities()
中获得。此方法提供了GrantedAuthority
对象的集合。授权是授予委托人的权限。此类权限通常是“roles”,可能是一个集合。
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {private final Collection<GrantedAuthority> authorities;@Overridepublic Collection<GrantedAuthority> getAuthorities() {return this.authorities;}}
而GrantedAuthority
的实现类SimpleGrantedAuthority
的getAuthority
可以获取到 “role” 。
public final class SimpleGrantedAuthority implements GrantedAuthority {@Overridepublic String getAuthority() {return this.role;}}
“roles” 有例如ROLE_ADMINISTRATOR
或ROLE_HR _SUPERVISOR
等。这些角色稍后可以在web授权、方法授权和域对象授权。 Spring Security 的其他部分能够使用这些权限。当使用基于 用户名/密码 的身份验证时,授权通常由UserDetailsService
加载。
通常,GrantedAuthority
对象的权限是整个应用程序范围。它们不是特定于给定域对象的。因此,不太可能拥有一个GrantedAuthority
来表示对 某个员工对象 的权限,因为如果有数千个这样的权限,将很快耗尽内存(或者,至少会导致应用程序花费很长时间来验证用户)。当然, Spring 安全性是专门为处理这一常见需求而设计的,可以使用项目的域对象安全功能来实现这一目的。
AuthenticationManager
AuthenticationManager
是定义 Spring Security 的过滤器如何执行身份验证的API。调用AuthenticationManager
的控制器(即Spring Security的过滤器)在SecurityContextHolder
上设置返回的身份验证。如果没有与 Spring Security 的过滤器集成,则可以直接设置SecurityContextHolder
,并且不需要使用AuthenticationManager
。
虽然AuthenticationManager
的实现可以是任何形式,但最常见的实现是ProviderManager
。
ProviderManager
ProviderManager
是AuthenticationManager
最常用的实现。ProviderManager
委托给 AuthenticationProviders 列表。每个 AuthenticationProvider 都有机会表明身份验证应该成功、失败,或者表明它无法做出决定,并允许下游 AuthenticationProvider 做出决定。如果配置的 AuthenticationProviders 都无法进行身份验证,则身份验证将失败,出现ProviderNotFoundException
,这是一种特殊的身份验证异常,表明ProviderManager
未配置为支持传递给它的身份验证类型。
实际上,每个 AuthenticationProvider 都知道如何执行特定类型的身份验证。例如,一个 AuthenticationProvider 可能能够验证用户名/密码,而另一个可能能够验证 SAML断言 。这允许每个 AuthenticationProvider 执行非常特定的身份验证类型,同时支持多种类型的身份验证,并且仅公开单个 AuthenticationManager bean 。
ProviderManager
还允许配置可选的父AuthenticationManager
,在没有 AuthenticationProvider 可以执行身份验证的情况下,可以咨询该父AuthenticationManager
。父级可以是任何类型的AuthenticationManager
,但它通常是ProviderManager
的实例。
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {Assert.notNull(providers, "providers list cannot be null");this.providers = providers;this.parent = parent;checkState();}
事实上,多个ProviderManager
实例可能共享同一个父AuthenticationManager
。这在多个SecurityFilterChain
实例具有一些共同身份验证(共享父身份验证管理器),但也具有不同的身份验证机制(不同的ProviderManager
实例)的场景中有些常见。
默认情况下,ProviderManager
将尝试从成功的身份验证请求返回的身份验证对象中清除任何敏感凭据信息。这可以防止密码等信息在 HttpSession 中保留的时间超过必要的时间。
例如,当使用用户对象缓存来提高无状态应用程序的性能时,这可能会导致问题。如果身份验证包含对缓存中对象(例如 UserDetails 实例)的引用,并且删除了其凭据,则将无法再根据缓存值进行身份验证。如果使用缓存,则需要考虑这一点。一个显而易见的解决方案是首先在缓存实现中或在创建返回的身份验证对象的 AuthenticationProvider 中创建对象的副本。或者,可以禁用ProviderManager
上的橡皮擦身份验证后的属性。
可以将多个 AuthenticationProviders 注入ProviderManager
。每个 AuthenticationProvider 执行特定类型的身份验证。例如,DaoAuthenticationProvider
支持基于用户名/密码的身份验证,而JwtAuthenticationProvider
支持对JWT令牌进行身份验证。
AuthenticationEntryPoint
AuthenticationEntryPoint
用于发送从客户端请求凭据的HTTP响应。
有时,客户端会主动包含凭据,如用户名/密码,以请求资源。在这些情况下, Spring Security 不需要提供从客户端请求凭据的HTTP响应,因为它们已经包含在内。
在其他情况下,客户端将对其无权访问的资源发出未经验证的请求。在这种情况下,AuthenticationEntryPoint
的实现用于从客户端请求凭据。AuthenticationEntryPoint
的实现可能会重定向到登录页面,并使用 WWW-Authenticate 标头进行响应,等等。
public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {private String realmName;@Overridepublic void afterPropertiesSet() {Assert.hasText(this.realmName, "realmName must be specified");}@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException {response.addHeader("WWW-Authenticate", "Basic realm=\"" + this.realmName + "\"");response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());}public String getRealmName() {return this.realmName;}public void setRealmName(String realmName) {this.realmName = realmName;}}
Handling Security Exceptions
ExceptionTranslationFilter
允许将 AccessDeniedException 和 AuthenticationException 转换为HTTP响应。
ExceptionTranslationFilter
作为安全过滤器之一插入FilterChainProxy。
ExceptionTranslationFilter
调用FilterChain.doFilter(request, response)
调用应用程序的其余部分。
如果用户未经身份验证或是身份验证异常,则启动身份验证。
清除SecurityContextHolder
HttpServletRequest 保存在 RequestCache 中。当用户成功进行身份验证时, RequestCache 用于重现原始请求。
AuthenticationEntryPoint
用于从客户端请求凭据。例如,它可能重定向到登录页面或发送 WWW-Authenticate 标头。
如果它是 AccessDeniedException ,则拒绝访问。调用 AccessDeniedHandler 来处理拒绝的访问。
如果应用程序未引发 AccessDeniedException 或 AuthenticationException ,则ExceptionTranslationFilter
不会执行任何操作。
ExceptionTranslationFilter
的伪代码如下所示:
try {filterChain.doFilter(request, response);} catch (AccessDeniedException | AuthenticationException ex) {if (!authenticated || ex instanceof AuthenticationException) {startAuthentication();} else {accessDenied();}}
将回调 FilterChain.doFilter(request, response) 相当于调用应用程序的其余部分。这意味着,如果应用程序的另一部分(即 FilterSecurityInterceptor 或方法安全性)引发 AuthenticationException 或 AccessDeniedException ,则会在此处捕获并处理。
如果用户未经身份验证或是身份验证异常,则启动身份验证。
否则,访问被拒绝。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
用作验证用户凭据的基本筛选器。在认证凭证之前,Spring Security 通常使用 AuthenticationEntryPoint 请求凭证。
AbstractAuthenticationProcessingFilter
可以对提交给它的任何身份验证请求进行身份验证。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dLoa8iRA-1657701149395)(https://docs.spring.io/spring-security/reference/_images/servlet/authentication/architecture/abstractauthenticationprocessingfilter.png)]
当用户提交其凭据时,AbstractAuthenticationProcessingFilter
从要进行身份验证的 HttpServletRequest 创建身份验证。创建的身份验证类型取决于AbstractAuthenticationProcessingFilter
的子类。例如,UsernamePasswordAuthenticationFilter
从 HttpServletRequest 中提交的用户名和密码创建 UsernamePasswordAuthenticationToken 。
身份验证被传递到AuthenticationManager
以进行身份验证。
第三,如果身份验证失败,则认证失败
清除 SecurityContextHolder
调用RememberMeServices.loginFail
调用AuthenticationFailureHandler
如果身份验证成功,则认证成功
SessionAuthenticationStrategy
收到新登录的通知
在SecurityContextHolder
上设置身份验证。然后SecurityContextPersistenceFilter
将 SecurityContext 保存到 HttpSession
调用RememberMeServices.loginSuccess
调用AuthenticationSuccessHandler
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBeanimplements ApplicationEventPublisherAware, MessageSourceAware {// ...private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {// 判断该filter是否能处理该次请求,即请求的路径和该filter配置要处理的url是否matchif (!requiresAuthentication(request, response)) {chain.doFilter(request, response);return;}try {Authentication authenticationResult = attemptAuthentication(request, response);if (authenticationResult == null) {//没有得到认证结果,表明子类实现中无法处理该类型的认证return;}this.sessionStrategy.onAuthentication(authenticationResult, request, response);// Authentication success// 认证成功后,通过设置属性值 continueChainBeforeSuccessfulAuthentication// 可以跳过认证成功后逻辑的处理if (this.continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}// 认证成功后处理successfulAuthentication(request, response, chain, authenticationResult);}catch (InternalAuthenticationServiceException failed) {this.logger.error("An internal error occurred while trying to authenticate the user.", failed);unsuccessfulAuthentication(request, response, failed);}catch (AuthenticationException ex) {// Authentication failedunsuccessfulAuthentication(request, response, ex);}}protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {if (this.requiresAuthenticationRequestMatcher.matches(request)) {return true;}if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));}return false;}// ...}
requiresAuthentication
此方法指示筛选器是否应尝试处理当前调用的登录请求。它从请求URL的“路径”部分中删除任何参数(例如https://host/myapp/index.html;jsessionid=blah),然后再与 filterProcessesUrl 属性进行匹配。
首先,AbstractAuthenticationProcessingFilter
调用chain.doFilter(request, response)
,即调用应用程序的其余部分(出现异常才执行自己的逻辑)。
认证成功后处理
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {SecurityContextHolder.getContext().setAuthentication(authResult);if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));}this.rememberMeServices.loginSuccess(request, response, authResult);if (this.eventPublisher != null) {this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}this.successHandler.onAuthenticationSuccess(request, response, authResult);}
如果验证成功:
在SecurityContextHolder
上设置成功的身份验证对象
通知已配置的RememberServices成功登录
通过配置的应用程序 EventPublisher 触发 InteractiveAuthenticationSuccessEvent
将其他行为委托给AuthenticationSuccessHandler
认证失败后处理
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException failed) throws IOException, ServletException {SecurityContextHolder.clearContext();this.logger.trace("Failed to process authentication request", failed);this.logger.trace("Cleared SecurityContextHolder");this.logger.trace("Handling authentication failure");this.rememberMeServices.loginFail(request, response);this.failureHandler.onAuthenticationFailure(request, response, failed);}
如果验证失败:
清除SecurityContextHolder
将异常存储在会话中(如果它存在或AllowessionCreation设置为true)
通知已配置的RememberServices登录失败
将其他行为委托给AuthenticationFailureHandler