ProviderManager
定义与作用
ProviderManager 是 AuthenticationManager 接口的默认实现类,也是 Spring Security 实际执行认证调度的核心。它内部维护一个 List<AuthenticationProvider>,通过遍历 + 委派的方式将认证请求分发给链中的具体 Provider。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private List<AuthenticationProvider> providers;
private AuthenticationManager parent; // 可选的父级认证管理器
// ...
}
手写过滤器的痛点
手写认证系统时,如果需要支持多种登录方式(如密码登录 + 短信登录 + 第三方 OAuth),通常写成一坨 if-else:
// 痛点:每新增一种认证方式,都要改这个方法的内部逻辑
public void login(String type, String credential) {
if ("password".equals(type)) {
// 查数据库比对密码
} else if ("sms".equals(type)) {
// 调用短信平台验证验证码
} else if ("oauth".equals(type)) {
// 调用第三方平台验证 access_token
}
// 违反开闭原则,扩展困难
}
ProviderManager 通过责任链模式彻底解耦:新增认证方式只需注册新的 AuthenticationProvider,无需改动 ProviderManager 本身的代码。
核心原理
遍历 Provider 的核心逻辑
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
// 1. 遍历内部所有 Provider
for (AuthenticationProvider provider : getProviders()) {
// 2. 先问 Provider:你支持处理这种 Token 吗?
if (!provider.supports(toTest)) {
continue; // 不支持,跳过
}
try {
// 3. 支持就尝试认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result); // 保留原始请求信息
break; // 认证成功,跳出循环
}
} catch (AccountStatusException | InternalAuthenticationServiceException e) {
// 账户状态异常直接抛出,不再尝试其他 Provider
throw e;
} catch (AuthenticationException e) {
lastException = e; // 记录异常,继续尝试下一个 Provider
}
}
// 4. 所有 Provider 都没成功,尝试父级 AuthenticationManager
if (result == null && parent != null) {
try {
result = parent.authenticate(authentication);
} catch (AuthenticationException e) {
parentException = e;
}
}
// 5. 彻底失败,抛出最后一个异常
if (result == null) {
throw lastException != null ? lastException : parentException;
}
// 6. 认证成功,可选:擦除凭据
if (eraseCredentialsAfterAuthentication) {
result = eraseCredentials(result);
}
return result;
}
核心流程图
父级委托机制(Parent)
ProviderManager 支持设置一个 parent(另一个 AuthenticationManager),当自身所有 Provider 都无法处理时,向上委托。这在多层级认证域中非常有用:
// 子 ProviderManager:只处理 JWT 认证
ProviderManager jwtProviderManager = new ProviderManager(
Collections.singletonList(jwtAuthenticationProvider)
);
// 父 ProviderManager:处理用户名密码认证
ProviderManager parentProviderManager = new ProviderManager(
Collections.singletonList(daoAuthenticationProvider)
);
// 子 Manager 设置父级
jwtProviderManager.setParent(parentProviderManager);
// 当请求不是 JWT 时,子 Manager 的 JWT Provider 不支持,会向上委托给父 Manager
示例一:注册多个 Provider 实现多因素登录
场景说明
系统需要支持三种登录方式:用户名密码、短信验证码、邮箱验证码。三种方式共用 ProviderManager,由它自动调度到对应的 Provider。
操作前配置
只有一个 DaoAuthenticationProvider,只能处理用户名密码:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
操作后配置
显式注册多个 Provider:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;
@Autowired
private EmailCodeAuthenticationProvider emailCodeAuthenticationProvider;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 1. 默认的 DaoAuthenticationProvider(处理用户名密码)
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
// 2. 追加短信验证码 Provider
auth.authenticationProvider(smsCodeAuthenticationProvider);
// 3. 追加邮箱验证码 Provider
auth.authenticationProvider(emailCodeAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/login/**", "/sms/**", "/email/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
}
}
自定义短信验证码 Provider:
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Autowired
private SmsCodeService smsCodeService;
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = authentication.getPrincipal().toString();
String code = authentication.getCredentials().toString();
// 验证短信码
if (!smsCodeService.verify(mobile, code)) {
throw new BadCredentialsException("短信验证码错误");
}
UserDetails user = userDetailsService.loadUserByUsername(mobile);
return new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
// 只处理 SmsCodeAuthenticationToken
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
// 自定义 Token 类型
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal; // 手机号
private final Object credentials; // 验证码
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
@Override
public Object getPrincipal() { return principal; }
@Override
public Object getCredentials() { return credentials; }
}
结果分析
ProviderManager内部现在有 3 个 Provider:默认的DaoAuthenticationProvider+SmsCodeAuthenticationProvider+EmailCodeAuthenticationProvider- 当用户提交表单密码时,
UsernamePasswordAuthenticationFilter构造UsernamePasswordAuthenticationToken,前两个 Provider 的supports()返回 false,DaoAuthenticationProvider返回 true,执行密码认证 - 当用户提交短信验证码时,自定义 Filter 构造
SmsCodeAuthenticationToken,只有SmsCodeAuthenticationProvider支持,执行短信认证
示例二:利用 Parent 实现全局兜底认证
场景说明
子系统 A(微服务)只处理 JWT 认证,但某些管理接口需要支持管理员用户名密码登录。将子系统的 ProviderManager 的 parent 指向一个共享的父 ProviderManager,实现认证兜底。
操作前配置
子系统只配置了 JWT Provider,管理员密码登录无法使用:
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(jwtAuthenticationProvider));
}
操作后配置
@Configuration
public class AuthenticationManagerConfig {
// 父级:处理用户名密码(部署在认证中心或共享模块)
@Bean
public AuthenticationManager parentAuthenticationManager(
UserDetailsService adminDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(adminDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(Collections.singletonList(provider));
}
// 子级:处理 JWT,同时设置 parent
@Bean
public AuthenticationManager jwtAuthenticationManager(
JwtAuthenticationProvider jwtAuthenticationProvider,
AuthenticationManager parentAuthenticationManager) {
ProviderManager providerManager = new ProviderManager(
Collections.singletonList(jwtAuthenticationProvider)
);
providerManager.setParent(parentAuthenticationManager);
return providerManager;
}
}
结果分析
- 子
ProviderManager只关心 JWT,但借助parent机制平滑复用了父级的密码认证能力 - 避免了在每个微服务里重复配置
DaoAuthenticationProvider
面试考点:Provider 遍历顺序与异常处理
问题
ProviderManager 遍历 Provider 时,如果第一个 Provider 抛出了 BadCredentialsException,后续 Provider 还会继续尝试吗?
答案
会继续尝试,但有一个例外:
// ProviderManager 源码中的异常分类
try {
result = provider.authenticate(authentication);
} catch (AccountStatusException | InternalAuthenticationServiceException e) {
throw e; // 账户状态异常直接抛出,不再尝试其他 Provider
} catch (AuthenticationException e) {
lastException = e; // 其他异常(如 BadCredentialsException)记录下来,继续尝试下一个
}
BadCredentialsException(密码错误)→ 继续下一个 ProviderAccountExpiredException/LockedException/DisabledException→ 立即终止,不再尝试其他 Provider
设计意图:账户状态异常是确定性的(用户确实被锁定),而密码错误可能是"这个 Provider 不认识这种密码格式",所以保留继续尝试的机会。
面试考点:
ProviderManager的parent机制在什么场景下使用?- 为什么
AccountStatusException会直接终止遍历,而BadCredentialsException不会?- 如何在多个 Provider 中控制优先级?(通过
configure()中authenticationProvider()的注册顺序)