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 请求(不推荐,有安全风险); -
用户名 / 密码参数名默认是
username和password,可通过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);
}
关键逻辑拆解:
-
obtainUsername和obtainPassword方法:默认从请求参数(request.getParameter(usernameParameter))中提取,若前端用 JSON 提交(如{"user":"xxx","pass":"xxx"}),需自定义实现(如通过ObjectMapper解析请求体); -
UsernamePasswordAuthenticationToken.unauthenticated(...):创建未认证状态的令牌(isAuthenticated()返回false); -
最终通过
this.getAuthenticationManager().authenticate(authRequest)将令牌交给认证管理器,过滤器的工作到此结束,后续校验由认证管理器负责。
二、实际校验逻辑:AuthenticationManager与DaoAuthenticationProvider
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方法最终会调用UserDetailsService的loadUserByUsername方法(用户自定义实现,如从 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 版本)
-
拦截请求 :
UsernamePasswordAuthenticationFilter拦截POST /login请求,通过attemptAuthentication方法提取用户名(username)和密码(password); -
创建令牌 :生成
UsernamePasswordAuthenticationToken(未认证状态); -
委托认证 :调用
AuthenticationManager(ProviderManager)的authenticate方法; -
分发校验 :
ProviderManager找到DaoAuthenticationProvider,调用其authenticate方法; -
查询用户 :
DaoAuthenticationProvider通过UserDetailsService.loadUserByUsername查询用户信息(校验用户名是否存在); -
比对密码 :通过
PasswordEncoder.matches比对提交的密码和数据库密文(校验密码是否正确); -
返回结果 :校验通过则生成已认证令牌 (
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