人人都会AI编程

15.2 用户名密码认证完整流程

更新时间:2026-07-10

在 Spring Security 中,用户名密码认证绝非简单的“接收参数 → 查数据库 → 返回结果”,而是一条由多个组件精密协作的过滤器链。理解这个流程,不仅能从容应对各类定制需求,也能在出现问题时快速定位症结。下面我们以最常见的 表单登录 为例,逐层剖析认证的完整流转。

15.2.1 请求进入过滤器链

当用户提交登录表单(POST /login,携带 username 和 password)时,请求首先进入 Spring Security 维护的 过滤器链(FilterChainProxy)。默认配置下,UsernamePasswordAuthenticationFilter 负责拦截该请求并进行处理。

// 过滤器默认拦截的 URL 及参数名
// 可通过 formLogin() 定制
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")

这个过滤器仅处理匹配的路径和 HTTP 方法(通常为 POST),其他请求直接放行。一旦命中,它会从请求中提取用户名和密码,构造尚未认证的 UsernamePasswordAuthenticationToken

15.2.2 构造未认证的令牌

提取的用户名和密码会被封装成 UsernamePasswordAuthenticationToken 实例,并设置为 未认证状态(authenticated = false)。此时令牌中仅包含主体(用户名)和凭证(密码),权限信息为空。

// 过滤器内部逻辑示意
String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken authRequest = 
    new UsernamePasswordAuthenticationToken(username, password);
// 可选:附加额外信息(如 remoteAddress、sessionId 等)
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));

之后,过滤器将令牌交给 AuthenticationManager 进行实际的认证处理。

15.2.3 AuthenticationManager 分发认证请求

AuthenticationManager 是认证工作的总调度接口,其默认实现 ProviderManager 管理着一系列 AuthenticationProvider。当认证请求到来时,它遍历所有 Provider,找到支持该令牌类型的 Provider 并委托其进行认证。

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
}

ProviderManager 的核心逻辑如下:

  1. 遍历内部的 providers 列表;
  2. 判断当前 provider.supports(tokenClass) 是否返回 true;
  3. 调用 provider.authenticate(authentication)
  4. 如果某个 Provider 认证成功,立即返回已认证的令牌;
  5. 若所有 Provider 都无法处理或认证失败,则抛出异常(如 BadCredentialsException)。

对于用户名密码登录,DaoAuthenticationProvider 是默认的 Provider,它专门处理 UsernamePasswordAuthenticationToken

15.2.4 DaoAuthenticationProvider 完成核心认证

DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider,其认证流程分为三步:

1. 加载用户详情(UserDetails)

Provider 调用注入的 UserDetailsService 接口:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) 
        throws UsernameNotFoundException;
}

开发者必须实现此接口,从数据库、LDAP 或其他用户存储中加载用户信息。返回的 UserDetails 对象包含用户名、密码(通常已加密)、账户状态(是否过期、锁定、启用)以及授予的权限列表。

// 示例:从数据库加载用户
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.isEnabled(),
                true, true, true,
                AuthorityUtils.createAuthorityList("ROLE_USER"));
    }
}

2. 预检查与密码校验

在密码比对之前,Provider 会执行一系列预检查:

  • 账户是否被锁定(isAccountNonLocked()
  • 账户是否启用(isEnabled()
  • 账户是否过期(isAccountNonExpired()

任一检查不通过,都将抛出对应的异常(如 LockedExceptionDisabledException)。

预检查通过后,进入密码校验环节。DaoAuthenticationProvider 内部持有一个 PasswordEncoder,它负责将用户提交的明文密码与 UserDetails 中存储的密文进行比对:

// 密码校验核心逻辑
String presentedPassword = authentication.getCredentials().toString();
String storedPassword = userDetails.getPassword();
if (!passwordEncoder.matches(presentedPassword, storedPassword)) {
    throw new BadCredentialsException("密码错误");
}

现代系统通常使用 BCrypt、SCrypt 或 Argon2 等自适应单向哈希算法,PasswordEncoder.matches() 方法会从存储的密文中提取盐值,对明文重新哈希后比较。

3. 后检查与返回已认证令牌

密码校验通过后,Provider 还会进行后检查(如凭证是否过期,isCredentialsNonExpired())。一切就绪,便创建一个全新的 UsernamePasswordAuthenticationToken,此时它的 authenticated 属性被设为 true,且包含 UserDetails 中的权限集合。

最后,ProviderManager 将已认证的令牌一路返回到 UsernamePasswordAuthenticationFilter

15.2.5 认证成功处理器(AuthenticationSuccessHandler)

过滤器收到已认证的令牌后,调用配置的 AuthenticationSuccessHandler 处理后续逻辑。默认实现 SimpleUrlAuthenticationSuccessHandler 会根据不同情况执行:

  • 如果是传统 Web 应用,通常重定向到先前访问的受保护页面,或跳转到预设的成功路径。
  • 如果是前后端分离应用,我们通常会自定义 SuccessHandler,返回 JSON 格式的用户信息和 token:
@Component
public class RestAuthenticationSuccessHandler 
        implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication) 
            throws IOException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("{\"code\":200,\"msg\":\"登录成功\"}");
    }
}

15.2.6 会话管理与安全上下文建立

认证成功后,Spring Security 还会将已认证的令牌存入 SecurityContext,并交由 SecurityContextRepository(默认基于 HTTP Session 的 HttpSessionSecurityContextRepository)持久化。下一次请求到来时,SecurityContextPersistenceFilter 会自动从会话中恢复认证信息,从而维持用户的登录状态。

对于无状态 RESTful API,可使用 SecurityContextHolder.setStrategyName("MODE_INHERITABLETHREADLOCAL") 或配合 JWT 过滤器在每次请求中重建令牌,而不依赖服务端会话。

15.2.7 认证失败及异常处理

认证过程中任何环节失败(用户不存在、密码错误、账户锁定等),对应的 Provider 会抛出AuthenticationException 的子类异常。UsernamePasswordAuthenticationFilter 捕获该异常后,调用 AuthenticationFailureHandler 进行处理。

@Component
public class RestAuthenticationFailureHandler 
        implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException exception) 
            throws IOException {
        response.setContentType("application/json;charset=utf-8");
        String msg;
        if (exception instanceof BadCredentialsException) {
            msg = "用户名或密码错误";
        } else if (exception instanceof LockedException) {
            msg = "账户已锁定";
        } else {
            msg = "认证失败";
        }
        response.getWriter().write("{\"code\":401,\"msg\":\"" + msg + "\"}");
    }
}

同时,框架可以配置缓存认证失败记录,防止暴力破解(如限制登录失败次数)。

15.2.8 完整流程图(文字版)

用户发起 POST /login
     │
     ▼
UsernamePasswordAuthenticationFilter
     │  提取 username, password
     │  构造未认证的 UsernamePasswordAuthenticationToken
     ▼
AuthenticationManager (ProviderManager)
     │  遍历 AuthenticationProvider
     ▼
DaoAuthenticationProvider
     ├─ UserDetailsService.loadUserByUsername(username)
     │     └─ 找不到 → UsernameNotFoundException
     ├─ 预检查(锁定、禁用、过期)
     ├─ PasswordEncoder.matches(明文, 密文)
     │     └─ 不匹配 → BadCredentialsException
     ├─ 后检查(凭证是否过期)
     └─ 返回已认证的 UsernamePasswordAuthenticationToken
     ▼
ProviderManager 返回已认证令牌
     ▼
AuthenticationSuccessHandler
     │  (重定向/返回JSON/生成Token)
     ▼
SecurityContextHolder 设置认证信息
SecurityContextRepository 持久化 Session

15.2.9 实用拓展:自定义认证流程

真实项目中,我们往往需要在不破坏整体架构的前提下注入自定义逻辑。常见的扩展点包括:

  • 自定义 UserDetailsService:对接不同的用户存储(数据库、LDAP、远程服务)。
  • 自定义 PasswordEncoder:实现特定的加解密算法。
  • 自定义 AuthenticationProvider:当需要额外的认证方式(如短信验证码、第三方登录)时,新增一个 Provider 并注册到 ProviderManager 中。
  • 自定义过滤器替换 UsernamePasswordAuthenticationFilter:例如从 JSON 请求体中提取用户名密码,而非默认的表单参数。
  • 认证事件监听:通过实现 ApplicationListener<AuthenticationSuccessEvent> 或使用 @EventListener 监听登录成功事件,用于记录登录日志、积分累计等。

掌握上述标准流程后,任何定制需求都能精准地“切”入对应的环节,而不会破坏整体安全体系。这正是 Spring Security 设计精妙之处——既提供了开箱即用的完整链路,又为每一段链路预留了清晰的扩展点。