likes
comments
collection
share

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

作者站长头像
站长
· 阅读数 318

源码分析:Spring Security 表单登录(下)

0、前言

代码环境是 Spring Boot 2.7.6

在(上)中,我们解析了表单登录前的一部分流程。先访问 /private ,由于未认证授权,会先跳转到表单登录页面。在这一部分中,我们解析从提交表单到登录成功这一过程中发生了什么。

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

  1. 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter 通过从 HttpServletRequest 实例中提取用户名和密码,创建一个 UsernamePasswordAuthenticationToken,这是一种 Authentication 类型。
  2. 接下来,UsernamePasswordAuthenticationToken 被传入 AuthenticationManager 实例,以进行认证。AuthenticationManager 的细节取决于 用户信息的存储方式
  3. 如果认证失败,则为 Failure.
    1. SecurityContextHolder 被清空。
    2. RememberMeServices.loginFail 被调用。如果没有配置remember me,这就是一个无用功。参见Javadoc中的 RememberMeServices 接口。
    3. AuthenticationFailureHandler 被调用。参见Javadoc中的 AuthenticationFailureHandler 类。
  4. 如果认证成功,则 Success
    1. SessionAuthenticationStrategy 被通知有新的登录。参见Javadoc中的 SessionAuthenticationStrategy 接口。
    2. Authentication 被设置在 SecurityContextHolder 上。参见 Javadoc 中的 SecurityContextPersistenceFilter 类。
    3. RememberMeServices.loginSuccess 被调用。如果没有配置remember me,这就是一个无用功。参见Javadoc中的 RememberMeServices 接口。
    4. ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent 事件。
    5. AuthenticationSuccessHandler 被调用。通常,这是一个 SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它会重定向到由 ExceptionTranslationFilter 保存的请求。

1、UsernamePasswordAuthenticationFilter 的执行逻辑

我们使用用户名 user 和 控制台打印的密码进行登录。

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前 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

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前 2)进入 UsernamePasswordAuthenticationFilterattemptAuthentication 方法中。

💡

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 的执行逻辑

进入 AuthenticationManagerauthenticate 方法。默认使用的 AuthenticationManagerProviderManager 。以下是 ProviderManagerauthenticate 方法。

/**
 * 使用多个认证提供者尝试对用户进行认证,遍历所有支持当前认证请求的提供者,
 * 并在成功时返回认证结果,如果所有提供者都未能成功认证,则抛出认证异常。
 *
 * @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;
}

从上面的代码可知,真正执行认证逻辑的是 AuthenticationProviderauthenticate 方法。

认证提供者是通过 getProviders() 方法获取的,默认会返回 AnonymousAuthenticationProvider ,它专门用于处理匿名用户的认证,只会支持 AnonymousAuthenticationToken 类型的认证对象。当前是 UsernamePasswordAuthenticationToken ,所以 AnonymousAuthenticationProvidersupport 方法返回 false

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前 如果所有提供者(这里仅有 AnonymousAuthenticationProvider 一个)都未成功认证,且存在父级认证管理器,尝试使用父级认证管理器认证。这里的父级认证管理器也是一个 ProviderManager ,它的 providersDaoAuthenticationProvider

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider 的实现类,专门用于处理 UsernamePasswordAuthenticationToken 的认证。这里可以发现下一步将要执行 DaoAuthenticationProviderauthenticate 方法。

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

3、DaoAuthenticationProvider 的执行逻辑

1)DaoAuthenticationProviderauthenticate 方法在父类 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)DaoAuthenticationProviderretrieveUser 方法。实际上是通过 UserDetailsServiceloadUserByUsername 方法中加载用户信息。

/**
 * 从用户详情服务加载用户信息,通过用户名获取对应的 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 的执行逻辑

InMemoryUserDetailsServiceloadUserByUsername 方法。

/**
 * 通过用户名加载用户详细信息。如果用户名不存在,则抛出 `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 加密的形式。

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

5、分析具体的认证流程

前边我们提到DaoAuthenticationProviderauthenticate 方法(位于父类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)代码执行返回到 UsernamePasswordAuthenticationFilterattemptAuthentication 方法,将认证结果返回。

/**
 * 尝试对传入的 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)认证成功后会重定向到 /

源码分析:Spring Security 表单登录(下)源码分析:Spring Security 表单登录(下) 0、前

转载自:https://juejin.cn/post/7425842010832404492
评论
请登录