AuthenticationManager
定义与作用
AuthenticationManager 是 Spring Security 认证体系的顶层入口接口,只定义了一个方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
所有认证请求——无论是表单登录、HTTP Basic、JWT 还是 OAuth2——最终都必须汇聚到 AuthenticationManager.authenticate() 才能被框架认可。它是认证流程的统一网关。
手写过滤器的痛点
手写登录时,认证逻辑通常散落在各个 Servlet 或 Controller 中:
// 痛点:认证逻辑分散,每个入口各写一套
@PostMapping("/api/login")
public ResponseEntity<?> apiLogin(@RequestBody LoginDto dto) {
// 自己查数据库、自己比对密码、自己组装 Session
}
@PostMapping("/web/login")
public String webLogin(@RequestParam String username, @RequestParam String password) {
// 又写一套几乎相同的逻辑
}
// 痛点:没有统一接口,无法做到"换数据源只改一处"
// 痛点:认证成功/失败后的处理逻辑无法复用
AuthenticationManager 通过单一接口聚合所有认证入口,让任何过滤器、任何控制器、任何外部系统都可以调用同一套认证能力。
核心原理
接口极简,职责单一
// 输入:一个未认证的 Authentication Token(通常只有用户名和密码)
// 输出:一个已认证的 Authentication(包含用户主体、权限列表、已认证标记)
// 异常:认证失败时抛出 AuthenticationException 子类
Authentication result = authenticationManager.authenticate(unauthenticatedToken);
类图与实现关系
AuthenticationManager 本身不处理任何具体认证逻辑,真正干活的是它的实现类 ProviderManager。ProviderManager 内部维护一个 AuthenticationProvider 列表,将认证请求委托给链中的某个 Provider 执行。这是一种典型的责任链 + 策略混合模式。
为什么设计成接口?
Spring Security 需要支持多种认证方式共存:表单登录、HTTP Basic、OAuth2、JWT、LDAP 等。每种方式的认证数据来源和校验规则不同,但都需要一个统一的调用入口。AuthenticationManager 作为接口,使得上层过滤器无需关心底层具体实现。
// UsernamePasswordAuthenticationFilter 内部
this.getAuthenticationManager().authenticate(authRequest);
// BasicAuthenticationFilter 内部
this.getAuthenticationManager().authenticate(authRequest);
// 两者调用的是同一个 AuthenticationManager,但背后的 Provider 可能不同
示例一:获取并使用 AuthenticationManager Bean
场景说明
在自定义 Controller 中手动触发认证(如双因素认证的第二步),需要直接调用 AuthenticationManager。
操作前配置
默认配置下,AuthenticationManager 由 Spring Boot 自动构建,但并未暴露为 Bean,外部无法直接注入:
// 错误尝试:直接注入会失败或拿到不期望的实例
@Autowired
private AuthenticationManager authenticationManager; // 可能报错或拿到null
操作后配置
显式将 AuthenticationManager 暴露为 Bean:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("alice").password("{noop}pass").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/mfa/**").authenticated()
.anyRequest().permitAll()
)
.formLogin(Customizer.withDefaults());
}
}
自定义 Controller 中调用:
@RestController
@RequestMapping("/mfa")
public class MfaController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/verify")
public ResponseEntity<?> verify(@RequestParam String code) {
// 第一步认证已在 Session 中,此处做第二步验证
// 实际场景:从 Session 取出临时 Token,再验证 OTP
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken("alice", code);
try {
Authentication result = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(result);
return ResponseEntity.ok("双因素认证通过");
} catch (BadCredentialsException e) {
return ResponseEntity.status(401).body("验证码错误");
}
}
}
结果分析
- 通过
authenticationManagerBean()暴露后,任何 Spring Bean 都可注入并调用认证能力 - 自定义认证流程(如短信验证码、图形验证码、双因素认证)可以复用框架的 Provider 链和异常体系
示例二:自定义 AuthenticationManager 实现隔离认证域
场景说明
系统中存在两套用户体系:普通用户(User)和管理后台用户(Admin),需要完全隔离的认证数据源和密码策略。
操作前配置
两套用户共用同一个 AuthenticationManager,导致代码中需要大量判断用户类型,耦合严重:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mixedUserDetailsService); // 一个Service里判断是User还是Admin
}
操作后配置
为两个域分别创建独立的 ProviderManager,通过 parent 关系实现层级委托:
@Configuration
@EnableWebSecurity
public class MultiRealmSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl(); // 普通用户数据源
}
@Bean
public UserDetailsService adminDetailsService() {
return new AdminDetailsServiceImpl(); // 管理员数据源
}
@Bean
public PasswordEncoder userPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PasswordEncoder adminPasswordEncoder() {
return new BCryptPasswordEncoder(12); // 管理员使用更高强度
}
// 普通用户认证管理器
@Bean
public AuthenticationManager userAuthenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService());
provider.setPasswordEncoder(userPasswordEncoder());
return new ProviderManager(Collections.singletonList(provider));
}
// 管理员认证管理器
@Bean
public AuthenticationManager adminAuthenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(adminDetailsService());
provider.setPasswordEncoder(adminPasswordEncoder());
return new ProviderManager(Collections.singletonList(provider));
}
// 过滤器链:普通用户
@Bean
@Order(1)
public SecurityFilterChain userChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/user/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.authenticationManager(userAuthenticationManager());
return http.build();
}
// 过滤器链:管理员
@Bean
@Order(2)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/admin/**")
.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN"))
.httpBasic(Customizer.withDefaults())
.authenticationManager(adminAuthenticationManager());
return http.build();
}
}
结果分析
/user/**的表单登录走userAuthenticationManager,查普通用户表,使用默认强度 BCrypt/admin/**的 Basic 认证走adminAuthenticationManager,查管理员表,使用强度 12 的 BCrypt- 两套认证域完全隔离,代码清晰,互不干扰
易错场景:忘记暴露 Bean 导致注入失败
问题描述
在 Spring Security 5.x 中,如果不显式调用 authenticationManagerBean(),直接在其他 Bean 中注入 AuthenticationManager 会报错或拿到 null。
错误代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 没有重写 authenticationManagerBean() 方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("test").password("{noop}test").roles("USER");
}
}
@Service
public class SomeService {
@Autowired
private AuthenticationManager authenticationManager; // 启动时报错:找不到类型为 AuthenticationManager 的 Bean
}
正确做法
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
面试考点:
AuthenticationManager为什么通常不直接暴露为 Bean?Spring Security 的WebSecurityConfigurerAdapter是如何构建内部AuthenticationManager的?