Spring Security 6.1.x 系列(5)—— Servlet 认证体系结构介绍
一、前言
本章主要学习Spring Security
中基于Servlet
的认证体系结构,为后续认证执行流程源码分析打好基础。
二、身份认证机制
Spring Security
提供个多种认证方式登录系统,包括:
- Username and Password:使用用户名/密码 方式进行认证登录
- OAuth 2.0 Login:使用
OpenID Connect
和OAuth 2.0
方式进行认证登录 - CAS: 使用
CAS
企业级单点登录系统 方式进行认证登录 - SAML 2.0 Login:使用
SAML 2.0
方式进行认证登录 - Remember Me:使用记住我 方式进行认证登录
- JAAS: 使用
JAAS
方式进行认证登录 - Pre-Authentication Scenarios:使用外部机制 方式进行认证登录
- X509:使用
X509
方式进行认证登录
三、认证组件简介
Spring Security
中的认证相关组件:
-
SecurityContextHolder:安全上下文持有者,存储当前认证用户的
SecurityContext
。 -
SecurityContext :安全上下文,包含当期认证用户的
Authentication
(认证信息),从SecurityContextHolder
中获取。 -
Authentication :认证信息,用户提供的用于身份认证的凭据的输入。
-
GrantedAuthority :授予用户的权限(角色、作用域)。
-
AuthenticationManager :认证管理器,是一个接口,定义
Spring Security
过滤器执行身份认证的API
。 -
ProviderManager :提供者管理器,是
AuthenticationManager
的默认实现。 -
AuthenticationProvider : 认证提供者,由
ProviderManager
选择,用于执行特定类型的身份认证。 -
AuthenticationEntryPoint : 认证入口点,处理认证过程中的认证异常,比如:重定向登录页面
-
AbstractAuthenticationProcessingFilter :抽象认证处理过滤器,一个
Filter
抽象类,是身份验证的基础。
四、认证组件源码分析
4.1 SecurityContextHolder
SecurityContextHolder
(安全上下文持有者)是Spring Security
身份认证模型的核心,存储已认证用户详细信息,包含了SecurityContext
(安全上下文):
当用户认证成功后,会将SecurityContext
设置到SecurityContextHolder
中,后续流程可以用过SecurityContextHolder
静态方法直接获取用户信息:
SecurityContext context = SecurityContextHolder.getContext();// 获取 SecurityContext
Authentication authentication = context.getAuthentication();// 获取认证信息
String username = authentication.getName(); // 用户名
Object principal = authentication.getPrincipal(); // 当前用户的信息,通常是UserDetails的实例
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();// 权限
默认策略下,SecurityContextHolder
使用ThreadLocal
来存储信息,一个已认证的请求线程在过滤器阶段,会获取会话中的认证信息,并保存在当前线程的ThreadLocal
中,方便业务代码获取用户信息,线程执行完毕后,FilterChainProxy
会自动执行清除。
某些应用程序并不完全适合使用默认策略 ,因为它们使用线程有特定方式。 例如:Swing
客户端可能希望Java
中的所有线程都使用相同的SecurityContextHolder
,您可以在启动时使用策略进行配置,以指定您希望如何存储SecurityContext
。
其他应用程序可能希望由安全线程派生的线程也采用相同的安全标识。 您可以通过两种方式更改默认模式:
- 第一种是设置系统属性,通过
System.getProperty("spring.security.strategy")
读取设置系统属性。 - 第二种是在调用静态方法,通过调用
SecurityContextHolder.setStrategyName(String strategyName)
设置。
SecurityContextHolder
提供的strategyName
有以下几种:
SecurityContextHolder.MODE_GLOBAL
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
SecurityContextHolder.MODE_THREADLOCAL
大多数应用程序不需要更改默认值。
4.2 SecurityContex
SecurityContex
是一个接口,从SecurityContextHolder
中获取,包含了Authentication
(认证信息),提供了两个简单方法:
public interface SecurityContext extends Serializable {
/**
* 获取当前已通过身份验证的主体或身份验证请求令牌。
*/
Authentication getAuthentication();
/**
* 更改当前已通过身份验证的主体或删除身份验证信息
*/
void setAuthentication(Authentication authentication);
}
SecurityContex
的实现类SecurityContextImpl
也很简单:
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private Authentication authentication;
public SecurityContextImpl() {
}
public SecurityContextImpl(Authentication authentication) {
this.authentication = authentication;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SecurityContextImpl) {
SecurityContextImpl other = (SecurityContextImpl) obj;
if ((this.getAuthentication() == null) && (other.getAuthentication() == null)) {
return true;
}
if ((this.getAuthentication() != null) && (other.getAuthentication() != null)
&& this.getAuthentication().equals(other.getAuthentication())) {
return true;
}
}
return false;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.authentication);
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append(" [");
if (this.authentication == null) {
sb.append("Null authentication");
}
else {
sb.append("Authentication=").append(this.authentication);
}
sb.append("]");
return sb.toString();
}
}
4.3 Authentication
Authentication
是一个接口,在Spring Security
中主要有两个作用:
- 预认证用户信息:此时没有经过认证,作为
AuthenticationManager
(认证管理器)的一个输入参数,用于提供认证凭证,在此时.isAuthenticated()
值为false
。 - 当前认证的用户:表示当前经过身份认证的用户,可以从
SecurityContext
获取当前的Authentication
(认证信息)
Authentication
包含以下信息:
-
principal
:用户主体标识。 使用用户名/密码进行身份验证时,这通常是UserDetails
的用户名。 -
credentials
:通常是密码。 在许多情况下,在用户通过身份验证后会清除此信息,以确保它不会泄露。 -
authorities
:GrantedAuthority
实例是授予用户的高级权限。 下文详细介绍。
Authentication
源码如下所示:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication
接口有很多实现类,对应不同的认证方式。比如:使用用户名/密码进行身份验证时,使用的是UsernamePasswordAuthenticationToken
。
4.4 GrantedAuthority
GrantedAuthority
实例是授予用户的权限,包括:角色、作用域。可以从Authentication.getAuthorities()
方法获取已认证用户的权限集合。
GrantedAuthority
源码如下所示:
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
只提供一个getAuthority()
方法,用于获取已认证用户的权限,默认实现类为SimpleGrantedAuthority
,源码如下所示:
public final class SimpleGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String role;
public SimpleGrantedAuthority(String role) {
Assert.hasText(role, "A granted authority textual representation is required");
this.role = role;
}
@Override
public String getAuthority() {
return this.role;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof SimpleGrantedAuthority sga) {
return this.role.equals(sga.getAuthority());
}
return false;
}
@Override
public int hashCode() {
return this.role.hashCode();
}
@Override
public String toString() {
return this.role;
}
}
4.5 AuthenticationManager
AuthenticationManager
(认证管理器)是一个接口,Spring Security
过滤器调用其认证方法进行身份认证,默认实现是ProviderManager
。
AuthenticationManager
源码如下所示:
/**
* Processes an {@link Authentication} request.
* 认证管理器 实现认证主要是通过AuthenticationManager接口
* 在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、
* 邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是AuthenticationManager。
*
* @author Ben Alex
*/
public interface AuthenticationManager {
/**
* authenticate()方法主要做三件事:
* 如果验证通过,返回Authentication(通常带上authenticated=true)。
* 认证失败抛出AuthenticationException
* 如果无法确定,则返回null
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
4.6 ProviderManager
ProviderManager
(提供者管理器)默认实现了AuthenticationManager
接口。其源码如下所示:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
/**
* Construct a {@link ProviderManager} using the given {@link AuthenticationProvider}s
* @param providers the {@link AuthenticationProvider}s to use
*/
public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), null);
}
/**
* Construct a {@link ProviderManager} using the given {@link AuthenticationProvider}s
* @param providers the {@link AuthenticationProvider}s to use
*/
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
/**
* Construct a {@link ProviderManager} using the provided parameters
* @param providers the {@link AuthenticationProvider}s to use
* @param parent a parent {@link AuthenticationManager} to fall back to
*/
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
@Override
public void afterPropertiesSet() {
checkState();
}
private void checkState() {
Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
"A parent AuthenticationManager or a list of AuthenticationProviders is required");
Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
"providers list cannot contain null values");
}
/**
* Attempts to authenticate the passed {@link Authentication} object.
* <p>
* The list of {@link AuthenticationProvider}s will be successively tried until an
* <code>AuthenticationProvider</code> indicates it is capable of authenticating the
* type of <code>Authentication</code> object passed. Authentication will then be
* attempted with that <code>AuthenticationProvider</code>.
* <p>
* If more than one <code>AuthenticationProvider</code> supports the passed
* <code>Authentication</code> object, the first one able to successfully authenticate
* the <code>Authentication</code> object determines the <code>result</code>,
* overriding any possible <code>AuthenticationException</code> thrown by earlier
* supporting <code>AuthenticationProvider</code>s. On successful authentication, no
* subsequent <code>AuthenticationProvider</code>s will be tried. If authentication
* was not successful by any supporting <code>AuthenticationProvider</code> the last
* thrown <code>AuthenticationException</code> will be rethrown.
* @param authentication the authentication request object.
* @return a fully authenticated object including credentials.
* @throws AuthenticationException if authentication fails.
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
/**
* Copies the authentication details from a source Authentication object to a
* destination one, provided the latter does not already have one set.
* @param source source authentication
* @param dest the destination authentication object
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
/**
* If set to, a resulting {@code Authentication} which implements the
* {@code CredentialsContainer} interface will have its
* {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called
* before it is returned from the {@code authenticate()} method.
* @param eraseSecretData set to {@literal false} to retain the credentials data in
* memory. Defaults to {@literal true}.
*/
public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
this.eraseCredentialsAfterAuthentication = eraseSecretData;
}
public boolean isEraseCredentialsAfterAuthentication() {
return this.eraseCredentialsAfterAuthentication;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
@Override
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
@Override
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}
ProviderManager
内部维护了多个AuthenticationProvider
(认证提供者),在ProviderManager
中的 authenticate(Authentication authentication)
方法中,每个AuthenticationProvider
实例都有一次机会去校验身份认证是否成功,或者是表明自己不能做出决定并将身份认证交给后面的AuthenticationProvider
实例处理。
如果所有的AuthenticationProvider
实例中没有一个可以处理当前类型认证的,则会抛出ProviderNotFoundException
异常导致认证失败。ProviderNotFoundException
是一个特殊的AuthenticationException
异常类型,用于说明ProviderManager
没有配置支持指定类型的认证。
实际上,每个AuthenticationProvider
实例都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider
可能能够验证用户名/密码,而另一个可能能够验证SAML
断言。
我们只需要实现一个AuthenticationManager
,并将我们所需要的 AuthenticationProvider
实例配置到里面,这样就能保证我们的程序支持多种验证类型。
ProviderManager
还允许配置一个可选的父级AuthenticationManager
,当AuthenticationProvider
实例无法执行身份验证时,可以咨询该父级AuthenticationManager
。父级AuthenticationManager
可以是任何类型的AuthenticationManager
,但它通常是ProviderManager
的一个实例。
实际上,多个ProviderManager
实列可能拥有共同的父级AuthenticationManager
。在有多个SecurityFilterChain
实例的场景中比较常见,这些SecurityFilterChain
实例有一些共同的身份验证(共享的父级AuthenticationManager
),但也有不同的身份验证机制(不同的ProviderManager
)。
默认情况下,尝试从成功的身份验证请求返回的对象中清除任何敏感凭据信息。 这样可以防止信息(如密码)在保留时限超过必要的时间。
4.7 AuthenticationProvider
AuthenticationProvider
(认证提供者)是一个接口,其中提供两个方式,其源码如下所示:
public interface AuthenticationProvider {
/**
*
* @param 身份验证请求对象。
* @return 包括凭证的完全经过身份验证的对象。
* 如果AuthenticationProvider无法支持对传递的authentication对象进行身份验证,
* 则可能返回null。在这种情况下,将尝试下一个支持所提供的Authentication类的AuthenticationProvider。
* AuthenticationException 如果身份验证失败抛出异常
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
*
* @param authentication。
* @return 如果此AuthenticationProvider支持特定类型的Authentication对象,则返回true。
* 返回true并不保证AuthenticationProvider能够对呈现的Authentication类实例进行身份验证。它只是表明它可以支持对其进行更密切的评估。
* AuthenticationProvider仍然可以从authenticate(Authentication authentication)方法返回null,以表明应该尝试下一个支持所提供的Authentication类的AuthenticationProvider。
* 能够执行身份验证的AuthenticationProvider的选择是在ProviderManager运行时进行的。
*/
boolean supports(Class<?> authentication);
}
可以在ProviderManager
中注入多个AuthenticationProvider
。每个AuthenticationProvider
执行特定类型的身份验证。
例如:
DaoAuthenticationProvider
支持基于用户名/密码的身份验证。JwtAuthenticationProvider
支持对JWT令牌的身份验证。
在Spring Security
中默认提供多个ProviderManager
实现类,如下图所示:
可以自定义扩展认证方式,例如:手机验证码等。
4.8 AuthenticationEntryPoint
AuthenticationEntryPoint
(认证入口点)用于 ExceptionTranslationFilter
发现认证异常时启动身份验证方案,例如:未经身份验证的请求,执行到登录页面的重定向。
AuthenticationEntryPoint
有以下实现类:
例如:LoginUrlAuthenticationEntryPoint
,会在表单登录失败或未经身份验证的请求,执行重定向(或转发)到登录表单页面的URL
。
4.9 AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
是一个抽象类,可以作为用户身份验证的基础,用户名/密码身份验证对应过滤器UsernamePasswordAuthenticationFilter
就是继承的AbstractAuthenticationProcessingFilter
。
例如:自定义手机验证码认证过滤器就可以通过继承AbstractAuthenticationProcessingFilter
进行开发,主要是提供认证成功、失败的相关处理:
①当用户提交凭证之后,AbstractAuthenticationProcessingFilter
通过HttpServletRequest
创建一个Authentication
用于进行身份验证。创建的Authentication
的具体类型依赖于AbstractAuthenticationProcessingFilter
的子类。
例如UsernamePasswordAuthenticationFilter
从在HttpServletRequest
中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken
。
②接着Authentication
对象将被传递到AuthenticationManager
中进行身份认证。
③如果认证失败:
SecurityContextHolder
被清空。RememberMeServices.loginFail
被调用,如果没有配置remember-me
,则不执行。AuthenticationFailureHandler
被调用。
④如果认证成功:
SessionAuthenticationStrategy
收到新登录的通知 。Authentication
被设置到SecurityContextHolder
,如果需要保存,以便在将来的请求中自动设置它,则必须显式调用SecurityContextRepository#saveContext
(详见下文)。RememberMeServices.loginSuccess
被调用,如果没有配置remember-me
,则不执行。ApplicationEventPublisher
发布InteractiveAuthenticationSuccessEvent
事件。AuthenticationSuccessHandler
被调用。