Spring Security 6.5.x 中用户名密码登录校验流程

Spring Security 6.5.5 中用户名密码登录校验流程

在 Spring Security 6.5.5 中,UsernamePasswordAuthenticationFilter依然是处理用户名 / 密码登录认证 的核心过滤器,但其核心职责仍是 "拦截登录请求、提取认证信息并触发认证流程",实际的用户名密码校验逻辑并不在该类中,而是通过 Spring Security 的 "认证管理器 - 认证提供者" 机制完成。以下结合 6.5.5 版本源码,详细拆解流程及校验位置。

一、UsernamePasswordAuthenticationFilter的核心职责与源码分析

UsernamePasswordAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,其核心作用是拦截符合条件的登录请求,提取用户名和密码,创建未认证令牌,并委托给 AuthenticationManager执行认证

1. 类定义与核心属性(6.5.5 版本)
java 复制代码
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 默认拦截的登录路径(可通过setFilterProcessesUrl修改)
    public static final String DEFAULT_FILTER_PROCESSES_URI = "/login";

    // 默认的用户名参数名(表单提交时的name属性,可通过setUsernameParameter修改)
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

    // 默认的密码参数名(同上)
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

    // 是否仅支持POST请求(默认true,可通过setPostOnly修改)
    private boolean postOnly = true;

    // 构造方法:默认拦截"/login",依赖AuthenticationManager
    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(DEFAULT_FILTER_PROCESSES_URI, "POST"), authenticationManager);
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    // 判断是否为需要处理的认证请求(默认:POST /login)
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }

    // 执行认证逻辑
    Authentication authResult = attemptAuthentication(request, response);
    // 认证成功后的处理(如设置安全上下文、跳转等)
    successfulAuthentication(request, response, chain, authResult);
    }
}

关键说明

  • 默认拦截POST /login请求,可通过setFilterProcessesUrl("/api/login")修改路径,通过setPostOnly(false)支持 GET 请求(不推荐,有安全风险);

  • 用户名 / 密码参数名默认是usernamepassword,可通过setUsernameParameter("user")适配前端表单(如 Vue/React 的参数名)。

2. 核心方法:attemptAuthentication(触发认证的入口)

attemptAuthentication()是过滤器的核心方法,负责从请求中提取认证信息,并委托给AuthenticationManager执行校验:

java 复制代码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

    // 校验请求方法:默认只支持POST,若为其他方法则抛出异常
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }

    // 从请求中提取用户名和密码
    String username = obtainUsername(request);
    String password = obtainPassword(request);

    // 处理空值(避免后续NPE)
    username = (username != null) ? username.trim() : "";
    password = (password != null) ? password : "";

    // 创建"未认证"的令牌(principal=用户名,credentials=密码,authorities=空集合)
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);

    // 设置请求详情(如IP地址、会话ID等,用于审计日志)
    setDetails(request, authRequest);

    // 核心:委托AuthenticationManager执行实际认证(校验逻辑在这里触发)
    return this.getAuthenticationManager().authenticate(authRequest);

}

关键逻辑拆解

  • obtainUsernameobtainPassword方法:默认从请求参数(request.getParameter(usernameParameter))中提取,若前端用 JSON 提交(如{"user":"xxx","pass":"xxx"}),需自定义实现(如通过ObjectMapper解析请求体);

  • UsernamePasswordAuthenticationToken.unauthenticated(...):创建未认证状态的令牌(isAuthenticated()返回false);

  • 最终通过this.getAuthenticationManager().authenticate(authRequest)将令牌交给认证管理器,过滤器的工作到此结束,后续校验由认证管理器负责

二、实际校验逻辑:AuthenticationManagerDaoAuthenticationProvider

UsernamePasswordAuthenticationFilter仅负责 "触发认证",而用户名密码的实际校验由 AuthenticationManager(认证管理器)及其管理的 AuthenticationProvider(认证提供者)完成 。在 6.5.5 版本中,默认的认证管理器是ProviderManager,核心的认证提供者是DaoAuthenticationProvider

1. ProviderManager:认证管理器的默认实现

ProviderManager管理多个AuthenticationProvider,其authenticate方法会遍历所有提供者,找到支持UsernamePasswordAuthenticationToken类型的提供者(即DaoAuthenticationProvider),并调用其authenticate方法:

java 复制代码
// ProviderManager.java(6.5.5版本核心逻辑)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;

    // 遍历所有AuthenticationProvider,找到支持当前令牌类型的提供者
    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }

        try {
            // 调用DaoAuthenticationProvider的authenticate方法执行校验
            result = provider.authenticate(authentication);
            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        } catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result == null && this.parent != null) {
        // 若当前管理器无结果,委托父管理器(如全局认证管理器)
        result = this.parent.authenticate(authentication);
    }
    return result;
}
2. DaoAuthenticationProvider:用户名密码校验的核心实现

DaoAuthenticationProvider是处理用户名 / 密码认证的核心提供者,继承自AbstractUserDetailsAuthenticationProvider,其校验逻辑分为两步:查询用户信息(校验用户名)比对密码(校验密码)

步骤 1:查询用户信息(校验用户名是否存在)

通过UserDetailsService从数据库 / 缓存中查询用户信息,若查询不到则抛出UsernameNotFoundException

java 复制代码
// AbstractUserDetailsAuthenticationProvider.java(6.5.5版本)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    String username = determineUsername(authentication); // 从令牌中获取用户名
    
    boolean cacheWasUsed = true;

    // 先从缓存中获取用户信息(若未缓存,则调用UserDetailsService查询)
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

        try {
            // 核心:调用UserDetailsService.loadUserByUsername查询用户
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        } catch (UsernameNotFoundException ex) {
            // 用户名不存在:抛出异常(登录失败)
            throw ex;
        }
    }

    // 步骤2:校验用户状态和密码(见下文)
    additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

    // 校验用户是否锁定、过期、禁用等
    preAuthenticationChecks.check(user);

    // ... 其他校验
    // 校验通过:创建已认证的令牌
    return createSuccessAuthentication(user.getUsername(), authentication, user);
}
  • retrieveUser方法最终会调用UserDetailsServiceloadUserByUsername方法(用户自定义实现,如从 MySQL 查询user表);

  • loadUserByUsername返回null或未找到用户,会抛出UsernameNotFoundException(前端表现为 "用户名不存在")。

步骤 2:比对密码(校验密码是否正确)

通过PasswordEncoder比对用户提交的密码和数据库中存储的加密密码:

java 复制代码
// DaoAuthenticationProvider.java(6.5.5版本)
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
    // 校验提交的密码是否为空
    if (authentication.getCredentials() == null) {
        throw new BadCredentialsException(this.messages.getMessage("DaoAuthenticationProvider.badCredentials", "Bad credentials"));
    }

    // 获取用户提交的密码(如明文"123456")
    String presentedPassword = authentication.getCredentials().toString();

    // 核心:用PasswordEncoder比对提交的密码和数据库中的加密密码(如"$2a$10$xxx...")
    if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {

        // 密码不匹配:抛出异常(登录失败)
        throw new BadCredentialsException(this.messages.getMessage("DaoAuthenticationProvider.badCredentials", "Bad credentials"));
    }
}

关键说明

  • PasswordEncoder是密码加密 / 比对的核心接口,6.5.5 版本默认使用BCryptPasswordEncoder(基于 BCrypt 算法加密);

  • matches方法会自动处理加密比对(如将用户提交的明文密码用相同盐值加密后,与数据库中的密文比对);

  • 若比对失败,抛出BadCredentialsException(前端表现为 "密码错误")。

三、登录校验完整流程(6.5.5 版本)

  1. 拦截请求UsernamePasswordAuthenticationFilter拦截POST /login请求,通过attemptAuthentication方法提取用户名(username)和密码(password);

  2. 创建令牌 :生成UsernamePasswordAuthenticationToken(未认证状态);

  3. 委托认证 :调用AuthenticationManagerProviderManager)的authenticate方法;

  4. 分发校验ProviderManager找到DaoAuthenticationProvider,调用其authenticate方法;

  5. 查询用户DaoAuthenticationProvider通过UserDetailsService.loadUserByUsername查询用户信息(校验用户名是否存在);

  6. 比对密码 :通过PasswordEncoder.matches比对提交的密码和数据库密文(校验密码是否正确);

  7. 返回结果 :校验通过则生成已认证令牌isAuthenticated()=true),并由successfulAuthentication方法处理后续(如设置SecurityContext、跳转首页);校验失败则抛出对应异常(如BadCredentialsException)。

四、总结:校验位置的核心结论

在 Spring Security 6.5.5 中:

  • UsernamePasswordAuthenticationFilter仅负责 "提取认证信息并触发认证流程",不直接参与用户名密码的校验

  • 用户名是否存在UserDetailsService.loadUserByUsername方法校验(查询数据库 / 缓存);

  • 密码是否正确DaoAuthenticationProvider.additionalAuthenticationChecks方法通过PasswordEncoder.matches校验。

这一设计符合 "单一职责原则":过滤器专注于请求处理,认证提供者专注于校验逻辑,便于扩展(如替换为手机号验证码登录,只需自定义过滤器和认证提供者)。

核心方法调用链路

java 复制代码
```java
|- org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter(jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse, jakarta.servlet.FilterChain)
    // 执行认证逻辑
    |- org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
        // 委托AuthenticationManager执行认证(实际校验在这里触发)
        |- org.springframework.security.authentication.ProviderManager#authenticate
            // 调用DaoAuthenticationProvider的authenticate方法
            |- org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
                // 调用UserDetailsService.loadUserByUsername查询用户
                |- org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
                    // 自定义UserDetailsService的实现类,通过用户名获取UserDetails对象
                    |- org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername 
                // 校验用户是否锁定、过期、禁用等
                |- org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks#check
                // 校验用户状态和密码
                |- org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#additionalAuthenticationChecks
                // 校验通过:创建已认证的令牌 
                |- org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication 
复制代码
相关推荐
文心快码BaiduComate1 天前
我用文心快码Spec 模式搓了个“pre作弊器”,妈妈再也不用担心我开会忘词了(附源码)
前端·后端·程序员
aiopencode1 天前
iOS 性能监控 运行时指标与系统行为的多工具协同方案
后端
E***U9451 天前
从新手到入门:如何判断自己是否真的学会了 Spring Boot
数据库·spring boot·后端
招风的黑耳1 天前
智慧养老项目:当SpringBoot遇到硬件,如何优雅地处理异常与状态管理?
java·spring boot·后端
回家路上绕了弯1 天前
分布式锁原理深度解析:从理论到实践
分布式·后端
磊磊磊磊磊1 天前
用AI做了个排版工具,分享一下如何高效省钱地用AI!
前端·后端·react.js
hgz07101 天前
Spring Boot Starter机制
java·spring boot·后端
daxiang120922051 天前
Spring boot服务启动报错 java.lang.StackOverflowError 原因分析
java·spring boot·后端
我家领养了个白胖胖1 天前
极简集成大模型!Spring AI Alibaba ChatClient 快速上手指南
java·后端·ai编程
一代明君Kevin学长1 天前
快速自定义一个带进度监控的文件资源类
java·前端·后端·python·文件上传·文件服务·文件流