源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前
源码分析:Spring Security 表单登录(下)
0、前言
代码环境是 Spring Boot 2.7.6
在(上)中,我们解析了表单登录前的一部分流程。先访问 /private
,由于未认证授权,会先跳转到表单登录页面。在这一部分中,我们解析从提交表单到登录成功这一过程中发生了什么。
- 当用户提交他们的用户名和密码时,
UsernamePasswordAuthenticationFilter
通过从HttpServletRequest
实例中提取用户名和密码,创建一个UsernamePasswordAuthenticationToken
,这是一种Authentication
类型。 - 接下来,
UsernamePasswordAuthenticationToken
被传入AuthenticationManager
实例,以进行认证。AuthenticationManager
的细节取决于 用户信息的存储方式。 - 如果认证失败,则为 Failure.
- SecurityContextHolder 被清空。
RememberMeServices.loginFail
被调用。如果没有配置remember me,这就是一个无用功。参见Javadoc中的RememberMeServices
接口。AuthenticationFailureHandler
被调用。参见Javadoc中的AuthenticationFailureHandler
类。
- 如果认证成功,则 Success。
SessionAuthenticationStrategy
被通知有新的登录。参见Javadoc中的SessionAuthenticationStrategy
接口。- Authentication 被设置在 SecurityContextHolder 上。参见 Javadoc 中的
SecurityContextPersistenceFilter
类。 RememberMeServices.loginSuccess
被调用。如果没有配置remember me,这就是一个无用功。参见Javadoc中的RememberMeServices
接口。ApplicationEventPublisher
发布InteractiveAuthenticationSuccessEvent
事件。AuthenticationSuccessHandler
被调用。通常,这是一个SimpleUrlAuthenticationSuccessHandler
,当我们重定向到登录页面时,它会重定向到由ExceptionTranslationFilter
保存的请求。
1、UsernamePasswordAuthenticationFilter
的执行逻辑
我们使用用户名
user
和 控制台打印的密码进行登录。
1)
UsernamePasswordAuthenticationFilter
的 doFilter
方法在父类 AbstractAuthenticationProcessingFilter
中。
/**
* 处理每个请求的认证过滤器方法。
* 它会检查是否需要认证,如果需要则尝试认证,
* 并根据认证结果执行后续操作。
*
* @param request HttpServletRequest 对象,包含客户端的请求信息
* @param response HttpServletResponse 对象,用于向客户端发送响应
* @param chain FilterChain 对象,用于将请求和响应传递给下一个过滤器
* @throws IOException 如果在过滤过程中发生 I/O 错误
* @throws ServletException 如果在过滤过程中发生 servlet 错误
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 检查该请求是否需要认证
if (!requiresAuthentication(request, response)) {
// 如果不需要认证,则将请求和响应传递给过滤链中的下一个过滤器
chain.doFilter(request, response);
return; // 直接返回,跳过后续的认证处理逻辑
}
try {
// 尝试进行认证操作
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// 如果认证结果为空,表示子类未完成认证,直接返回
return;
}
// 使用 session 策略处理成功的认证
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// 如果设置为在成功认证之前继续执行过滤链
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response); // 继续执行过滤链
}
// 处理成功的认证操作
successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException failed) {
// 捕获内部认证服务异常,记录错误日志
this.logger.error("在尝试认证用户时发生内部错误。", failed);
// 处理认证失败的情况
unsuccessfulAuthentication(request, response, failed);
} catch (AuthenticationException ex) {
// 捕获认证异常,处理认证失败的情况
unsuccessfulAuthentication(request, response, ex);
}
}
requiresAuthentication()
方法返回true
。
2)进入
UsernamePasswordAuthenticationFilter
的 attemptAuthentication
方法中。
attemptAuthentication
的功能是尝试进行认证操作。
- 从 request 中获取用户名和密码
- 用获取到的用户名和密码封装一个未认证的 “token” ,即
UsernamePasswordAuthenticationToken
。 - 利用
AuthenticationManager
来执行认证操作。
/**
* 尝试对传入的 HTTP 请求进行用户认证。
* 该方法提取请求中的用户名和密码,并将其封装为未认证的 UsernamePasswordAuthenticationToken,
* 然后使用认证管理器执行认证。
*
* @param request HttpServletRequest 对象,包含客户端的请求信息,尤其是用户名和密码
* @param response HttpServletResponse 对象,用于向客户端发送响应
* @return Authentication 对象,表示认证成功的用户信息
* @throws AuthenticationException 如果认证失败,抛出认证异常
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 检查是否仅允许 POST 请求进行认证,如果当前请求不是 POST 请求且 postOnly 为 true,则抛出异常
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("认证方法不支持: " + request.getMethod());
}
// 从请求中获取用户名,调用自定义的 obtainUsername 方法
String username = obtainUsername(request);
// 如果用户名不为空,则去除多余的空格;如果为空,则设为空字符串
username = (username != null) ? username.trim() : "";
// 从请求中获取密码,调用自定义的 obtainPassword 方法
String password = obtainPassword(request);
// 如果密码为空,则设为空字符串
password = (password != null) ? password : "";
// 创建未认证的 UsernamePasswordAuthenticationToken 实例,封装用户名和密码
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
// 允许子类设置认证请求的详细信息,比如 IP 地址、会话 ID 等
setDetails(request, authRequest);
// 调用认证管理器对封装好的认证请求进行认证,并返回认证结果
return this.getAuthenticationManager().authenticate(authRequest);
}
2、AuthenticationManager
的执行逻辑
进入 AuthenticationManager
的 authenticate
方法。默认使用的 AuthenticationManager
是 ProviderManager
。以下是 ProviderManager
的 authenticate
方法。
/**
* 使用多个认证提供者尝试对用户进行认证,遍历所有支持当前认证请求的提供者,
* 并在成功时返回认证结果,如果所有提供者都未能成功认证,则抛出认证异常。
*
* @param authentication 传入的 Authentication 对象,包含用户提交的认证请求(如用户名和密码)
* @return Authentication 返回成功认证后的 Authentication 对象,表示已认证的用户信息
* @throws AuthenticationException 如果认证失败,抛出相关的认证异常
*/
@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("使用 %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);
throw ex;
}
catch (AuthenticationException ex) {
// 捕获认证异常,记录为最后一次异常,继续尝试其他认证提供者
lastException = ex;
}
}
// 如果所有提供者都未成功认证,且存在父级认证管理器,尝试使用父级认证管理器认证
if (result == null && this.parent != null) {
try {
// 调用父级认证管理器进行认证
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException ex) {
// 如果父级认证管理器未找到支持的提供者,忽略该异常
} catch (AuthenticationException ex) {
// 捕获父级认证管理器抛出的认证异常
parentException = ex;
lastException = ex;
}
}
// 如果认证成功,清除认证凭证并发布认证成功事件
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// 认证完成后,清除认证凭证等敏感信息
((CredentialsContainer) result).eraseCredentials();
}
// 如果父级认证管理器未认证成功,则发布认证成功事件
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
// 返回认证成功的结果
return result;
}
// 如果认证失败,且未捕获任何异常,则抛出 ProviderNotFoundException
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage(
"ProviderManager.providerNotFound", new Object[] { toTest.getName() },
"未找到支持 {0} 的认证提供者"));
}
// 如果父级认证管理器认证失败,处理认证失败的异常
if (parentException == null) {
prepareException(lastException, authentication);
}
// 抛出最后捕获的认证异常
throw lastException;
}
从上面的代码可知,真正执行认证逻辑的是 AuthenticationProvider
的 authenticate
方法。
认证提供者是通过 getProviders()
方法获取的,默认会返回 AnonymousAuthenticationProvider
,它专门用于处理匿名用户的认证,只会支持 AnonymousAuthenticationToken
类型的认证对象。当前是 UsernamePasswordAuthenticationToken
,所以 AnonymousAuthenticationProvider
的 support
方法返回 false
。
如果所有提供者(这里仅有
AnonymousAuthenticationProvider
一个)都未成功认证,且存在父级认证管理器,尝试使用父级认证管理器认证。这里的父级认证管理器也是一个 ProviderManager
,它的 providers
是 DaoAuthenticationProvider
。
DaoAuthenticationProvider
是 AbstractUserDetailsAuthenticationProvider
的实现类,专门用于处理 UsernamePasswordAuthenticationToken
的认证。这里可以发现下一步将要执行 DaoAuthenticationProvider
的 authenticate
方法。
3、DaoAuthenticationProvider
的执行逻辑
1)DaoAuthenticationProvider
的 authenticate
方法在父类 AbstractUserDetailsAuthenticationProvider
中。
/**
* 该方法使用提供的认证信息(用户名和密码)尝试对用户进行认证。
* 它首先检查缓存中是否存在用户信息,如果不存在则从数据库或其他持久层中检索用户信息,
* 之后进行认证前和认证后的检查,并根据认证结果返回一个成功的 Authentication 对象。
*
* @param authentication 包含用户凭证的 Authentication 对象
* @return 认证成功后返回一个包含用户详细信息的 Authentication 对象
* @throws AuthenticationException 如果认证失败,抛出相应的认证异常
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 确保传入的 authentication 对象是 UsernamePasswordAuthenticationToken 类型
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"仅支持 UsernamePasswordAuthenticationToken 类型"));
// 从传入的认证对象中获取用户名
String username = determineUsername(authentication);
// 用于标记缓存是否被使用
boolean cacheWasUsed = true;
// 尝试从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
// 如果缓存中没有找到用户信息,则从持久层检索用户信息
if (user == null) {
cacheWasUsed = false;
try {
// 从数据库或其他持久层中检索用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException ex) {
// 如果用户不存在,记录日志
this.logger.debug("未找到用户 '" + username + "'");
// 根据配置决定是否隐藏用户不存在的异常
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
// 抛出凭证错误异常
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "无效的凭证"));
}
// 确保 retrieveUser 方法不会返回 null,违背接口约定则抛出异常
Assert.notNull(user, "retrieveUser 返回 null,违反了接口约定");
}
try {
// 进行认证前的检查(如账户是否锁定等)
this.preAuthenticationChecks.check(user);
// 进行额外的认证检查(验证密码等)
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException ex) {
// 如果认证失败且未使用缓存,再次从持久层检索用户并重试认证
if (!cacheWasUsed) {
throw ex;
}
// 重新从持久层检索最新的用户数据并重试
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 认证成功后,进行认证后的检查(如密码过期检查等)
this.postAuthenticationChecks.check(user);
// 如果未使用缓存,则将用户信息放入缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
// 如果配置为将 principal 强制转换为字符串,则返回用户名,否则返回 UserDetails 对象
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建成功的认证对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
authenticate
方法中会优先从缓存中获取用户信息,如果缓存中没有找到用户信息,则调用 retrieveUser
方法从持久层检索用户信息。
2)DaoAuthenticationProvider
的 retrieveUser
方法。实际上是通过 UserDetailsService
的 loadUserByUsername
方法中加载用户信息。
/**
* 从用户详情服务加载用户信息,通过用户名获取对应的 UserDetails 对象。
* 该方法在加载用户信息时进行了一些安全措施,如防止时间攻击,并处理不同的异常情况。
*
* @param username 用户名,用于加载用户信息
* @param authentication 包含用户凭证的认证对象(如用户名和密码)
* @return UserDetails 返回加载的用户信息
* @throws AuthenticationException 如果发生认证异常,抛出相应的异常
*/
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 准备时间攻击防护机制,确保认证过程不会泄露时间上的差异
prepareTimingAttackProtection();
try {
// 从 UserDetailsService 中加载用户信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
// 如果加载的用户信息为空,抛出内部认证服务异常
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService 返回 null,违反了接口约定");
}
// 返回加载的用户信息
return loadedUser;
}
catch (UsernameNotFoundException ex) {
// 如果未找到用户,进行时间攻击防护,并抛出用户名未找到异常
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
// 如果是内部认证服务异常,直接抛出该异常
throw ex;
}
catch (Exception ex) {
// 捕获其他所有异常,并抛出内部认证服务异常
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
UserDetailsService
的默认实现是 InMemoryUserDetailsService
,表示从内存中加载用户信息。
4、UserDetailsService
的执行逻辑
InMemoryUserDetailsService
的 loadUserByUsername
方法。
/**
* 通过用户名加载用户详细信息。如果用户名不存在,则抛出 `UsernameNotFoundException`。
* 该方法从内部存储(如内存中的 `users` 映射)中获取用户信息,并返回一个包含用户详细信息的 `User` 对象。
*
* @param username 要加载的用户名
* @return UserDetails 返回一个包含用户详细信息的 `User` 对象
* @throws UsernameNotFoundException 如果用户名不存在,则抛出此异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 将用户名转换为小写以保证不区分大小写
UserDetails user = this.users.get(username.toLowerCase());
// 如果用户信息不存在,抛出用户名未找到异常
if (user == null) {
throw new UsernameNotFoundException(username);
}
// 如果找到用户信息,创建并返回一个 User 对象,包含用户的详细信息
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(),
user.getAuthorities());
}
在InMemoryUserDetailsService
中维护了一个 Map
,保存了用户信息。我们看到的这个用户信息,其实就是 Spring Security 默认用户。用户名是 user,密码是打印在控制台的密码的 Bcrypt 加密的形式。
5、分析具体的认证流程
前边我们提到DaoAuthenticationProvider
的authenticate
方法(位于父类AbstractUserDetailsAuthenticationProvider
中)中会优先从缓存中获取用户信息,如果缓存中没有找到用户信息,则调用 retrieveUser
方法从持久层检索用户信息。现在已经从 InMemoryUserDetailsService
中检索到了用户信息。
/**
* 该方法使用提供的认证信息(用户名和密码)尝试对用户进行认证。
* 它首先检查缓存中是否存在用户信息,如果不存在则从数据库或其他持久层中检索用户信息,
* 之后进行认证前和认证后的检查,并根据认证结果返回一个成功的 Authentication 对象。
*
* @param authentication 包含用户凭证的 Authentication 对象
* @return 认证成功后返回一个包含用户详细信息的 Authentication 对象
* @throws AuthenticationException 如果认证失败,抛出相应的认证异常
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 确保传入的 authentication 对象是 UsernamePasswordAuthenticationToken 类型
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"仅支持 UsernamePasswordAuthenticationToken 类型"));
// 从传入的认证对象中获取用户名
String username = determineUsername(authentication);
// 用于标记缓存是否被使用
boolean cacheWasUsed = true;
// 尝试从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
// 如果缓存中没有找到用户信息,则从持久层检索用户信息
if (user == null) {
cacheWasUsed = false;
try {
// 从数据库或其他持久层中检索用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException ex) {
// 如果用户不存在,记录日志
this.logger.debug("未找到用户 '" + username + "'");
// 根据配置决定是否隐藏用户不存在的异常
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
// 抛出凭证错误异常
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "无效的凭证"));
}
// 确保 retrieveUser 方法不会返回 null,违背接口约定则抛出异常
Assert.notNull(user, "retrieveUser 返回 null,违反了接口约定");
}
try {
// 进行认证前的检查(如账户是否锁定等)
this.preAuthenticationChecks.check(user);
// 进行额外的认证检查(验证密码等)
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException ex) {
// 如果认证失败且未使用缓存,再次从持久层检索用户并重试认证
if (!cacheWasUsed) {
throw ex;
}
// 重新从持久层检索最新的用户数据并重试
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 认证成功后,进行认证后的检查(如密码过期检查等)
this.postAuthenticationChecks.check(user);
// 如果未使用缓存,则将用户信息放入缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
// 如果配置为将 principal 强制转换为字符串,则返回用户名,否则返回 UserDetails 对象
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建成功的认证对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
接下来会对检索到的用户信息进行认证操作。
首先会进行认证前的检查 this.preAuthenticationChecks.check(user);
(如账户是否锁定等),其次进行一些额外的认证检查 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication)
(验证密码等)。
在 additionalAuthenticationChecks
方法中会将检索到的用户信息 user
同认证信息authentication
相比较。具体来说就是会利用 PasswordEncoder
将认证信息中的密码与检索出来的用户信息的密码相比较,判断是否匹配。
/**
* 进行额外的认证检查,验证用户提供的凭证(如密码)是否有效。
* 该方法会检查是否提供了凭证(密码),并将提供的密码与存储的密码进行比对,如果不匹配则抛出 `BadCredentialsException`。
*
* @param userDetails 包含用户详细信息的 `UserDetails` 对象
* @param authentication 包含用户凭证的 `UsernamePasswordAuthenticationToken` 对象
* @throws AuthenticationException 如果认证失败,抛出相应的异常(如密码不匹配)
*/
@Override
@SuppressWarnings("deprecation") // 忽略弃用警告(可能是因为使用了旧的密码编码器)
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 如果凭证为空,抛出认证失败异常
if (authentication.getCredentials() == null) {
this.logger.debug("认证失败,因为未提供凭证");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "无效的凭证"));
}
// 获取用户提供的密码
String presentedPassword = authentication.getCredentials().toString();
// 检查提供的密码与存储的密码是否匹配
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("认证失败,因为密码与存储的密码不匹配");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "无效的凭证"));
}
}
完成认证前的检查 this.preAuthenticationChecks.check(user);
和额外的认证检查 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication)
后,会再执行认证后检查 this.postAuthenticationChecks.check(user);
一切都执行成功后,执行 createSuccessAuthentication(principalToReturn, authentication, user);
方法创建成功的认证对象并返回。
/**
* 创建并返回一个表示认证成功的 `Authentication` 对象。
* 该方法确保返回用户原始凭证,并保留用户提供的详细信息(如认证过程中的原始凭证和附加细节)。
*
* @param principal 认证成功后返回的主体信息(通常是用户信息)
* @param authentication 包含用户凭证和认证细节的原始 `Authentication` 对象
* @param user 加载的 `UserDetails` 对象,包含用户的详细信息
* @return 返回一个成功认证的 `Authentication` 对象,包含认证信息、凭证和权限
*/
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// 确保返回用户提供的原始凭证,以便即使使用了加密密码,后续尝试仍然成功
// 同时确保返回原始的认证细节,以便缓存过期后,未来的认证事件能够包含详细信息
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
// 设置认证细节(比如认证时提供的额外信息)
result.setDetails(authentication.getDetails());
// 记录认证成功的日志
this.logger.debug("用户认证成功");
// 返回成功认证后的结果
return result;
}
6、认证成功后的操作
1)到这已经算是认证成功了,代码执行返回到 ProviderManager
中。认证成功后,清除认证凭证并发布认证成功事件。
/**
* 使用多个认证提供者尝试对用户进行认证,遍历所有支持当前认证请求的提供者,
* 并在成功时返回认证结果,如果所有提供者都未能成功认证,则抛出认证异常。
*
* @param authentication 传入的 Authentication 对象,包含用户提交的认证请求(如用户名和密码)
* @return Authentication 返回成功认证后的 Authentication 对象,表示已认证的用户信息
* @throws AuthenticationException 如果认证失败,抛出相关的认证异常
*/
@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("使用 %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);
throw ex;
}
catch (AuthenticationException ex) {
// 捕获认证异常,记录为最后一次异常,继续尝试其他认证提供者
lastException = ex;
}
}
// 如果所有提供者都未成功认证,且存在父级认证管理器,尝试使用父级认证管理器认证
if (result == null && this.parent != null) {
try {
// 调用父级认证管理器进行认证
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException ex) {
// 如果父级认证管理器未找到支持的提供者,忽略该异常
} catch (AuthenticationException ex) {
// 捕获父级认证管理器抛出的认证异常
parentException = ex;
lastException = ex;
}
}
// 如果认证成功,清除认证凭证并发布认证成功事件
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// 认证完成后,清除认证凭证等敏感信息
((CredentialsContainer) result).eraseCredentials();
}
// 如果父级认证管理器未认证成功,则发布认证成功事件
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
// 返回认证成功的结果
return result;
}
// 如果认证失败,且未捕获任何异常,则抛出 ProviderNotFoundException
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage(
"ProviderManager.providerNotFound", new Object[] { toTest.getName() },
"未找到支持 {0} 的认证提供者"));
}
// 如果父级认证管理器认证失败,处理认证失败的异常
if (parentException == null) {
prepareException(lastException, authentication);
}
// 抛出最后捕获的认证异常
throw lastException;
}
2)代码执行返回到 UsernamePasswordAuthenticationFilter
的 attemptAuthentication
方法,将认证结果返回。
/**
* 尝试对传入的 HTTP 请求进行用户认证。
* 该方法提取请求中的用户名和密码,并将其封装为未认证的 UsernamePasswordAuthenticationToken,
* 然后使用认证管理器执行认证。
*
* @param request HttpServletRequest 对象,包含客户端的请求信息,尤其是用户名和密码
* @param response HttpServletResponse 对象,用于向客户端发送响应
* @return Authentication 对象,表示认证成功的用户信息
* @throws AuthenticationException 如果认证失败,抛出认证异常
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 检查是否仅允许 POST 请求进行认证,如果当前请求不是 POST 请求且 postOnly 为 true,则抛出异常
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("认证方法不支持: " + request.getMethod());
}
// 从请求中获取用户名,调用自定义的 obtainUsername 方法
String username = obtainUsername(request);
// 如果用户名不为空,则去除多余的空格;如果为空,则设为空字符串
username = (username != null) ? username.trim() : "";
// 从请求中获取密码,调用自定义的 obtainPassword 方法
String password = obtainPassword(request);
// 如果密码为空,则设为空字符串
password = (password != null) ? password : "";
// 创建未认证的 UsernamePasswordAuthenticationToken 实例,封装用户名和密码
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
// 允许子类设置认证请求的详细信息,比如 IP 地址、会话 ID 等
setDetails(request, authRequest);
// 调用认证管理器对封装好的认证请求进行认证,并返回认证结果
return this.getAuthenticationManager().authenticate(authRequest);
}
3)认证成功后,在 AbstractAuthenticationProcessingFilter
中执行 successfulAuthentication
认证成功的逻辑。
/**
* 处理认证成功后的逻辑。该方法将成功的认证信息保存到 `SecurityContextHolder` 中,并触发一些额外的处理逻辑,如记住我功能和事件发布。
*
* @param request 当前的 `HttpServletRequest`,包含请求的详细信息
* @param response 当前的 `HttpServletResponse`,用于返回响应信息
* @param chain 当前的过滤器链,用于继续请求处理
* @param authResult 认证成功后的 `Authentication` 对象,包含用户的认证信息
* @throws IOException 如果在处理请求或响应时发生 I/O 错误
* @throws ServletException 如果处理过程中发生了 servlet 异常
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 创建一个空的 SecurityContext 并将认证结果放入其中
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
// 将新的 SecurityContext 设置到 SecurityContextHolder 中,供当前线程使用
SecurityContextHolder.setContext(context);
// 将 SecurityContext 保存到指定的存储库中,以便后续请求可以获取认证信息
this.securityContextRepository.saveContext(context, request, response);
// 如果日志级别为调试,记录认证成功的详细信息
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("将 SecurityContextHolder 设置为 %s", authResult));
}
// 触发记住我服务的登录成功逻辑,通常是设置 cookie 等
this.rememberMeServices.loginSuccess(request, response, authResult);
// 如果事件发布器不为空,发布一个交互式认证成功事件
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 调用认证成功处理器,执行认证成功后的处理逻辑(如重定向或返回响应)
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
4)认证成功后会重定向到 /
。
转载自:https://juejin.cn/post/7425842010832404492