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 
复制代码
相关推荐
最贪吃的虎8 小时前
Redis其实并不是线程安全的
java·开发语言·数据库·redis·后端·缓存·lua
武子康8 小时前
大数据-208 岭回归与Lasso回归:区别、应用与选择指南
大数据·后端·机器学习
qq_12498707538 小时前
基于springboot归家租房小程序的设计与实现(源码+论文+部署+安装)
java·大数据·spring boot·后端·小程序·毕业设计·计算机毕业设计
moxiaoran57538 小时前
Go语言的接口
开发语言·后端·golang
清风徐来QCQ8 小时前
Cookie和JWT
后端·cookie
2301_780669868 小时前
List(特有方法、遍历方式、ArrayList底层原理、LinkedList底层原理,二者区别)
java·数据结构·后端·list
浮尘笔记8 小时前
Go语言中的同步等待组和单例模式:sync.WaitGroup和sync.Once
开发语言·后端·单例模式·golang
有梦想的攻城狮9 小时前
Django使用介绍
后端·python·django
IT_陈寒9 小时前
2025年React生态最新趋势:我从Redux迁移到Zustand后性能提升40%的心得
前端·人工智能·后端
superman超哥9 小时前
Rust VecDeque 的环形缓冲区设计:高效双端队列的奥秘
开发语言·后端·rust·rust vecdeque·环形缓冲区设计·高效双端队列