乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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(密码错误)→ 继续下一个 Provider
  • AccountExpiredException / LockedException / DisabledException → 立即终止,不再尝试其他 Provider

设计意图:账户状态异常是确定性的(用户确实被锁定),而密码错误可能是"这个 Provider 不认识这种密码格式",所以保留继续尝试的机会。

面试考点:

  • ProviderManager 的 parent 机制在什么场景下使用?
  • 为什么 AccountStatusException 会直接终止遍历,而 BadCredentialsException 不会?
  • 如何在多个 Provider 中控制优先级?(通过 configure() 中 authenticationProvider() 的注册顺序)
上一页
AuthenticationManager
下一页
DaoAuthenticationProvider