1. 概述
本文将介绍如何在 Spring Security 中配置多种认证机制,通过组合多个认证提供者(Authentication Provider)实现灵活的用户认证方案。
2. 认证提供者原理
AuthenticationProvider
是 Spring Security 的核心抽象接口,用于从特定存储源(如数据库、LDAP 或自定义第三方系统)获取用户信息,并验证用户凭证。
⚠️ 关键特性:
- 当配置多个认证提供者时,系统会按声明顺序依次尝试认证
- 只要任意一个提供者认证成功,即视为整体认证通过
- 所有提供者均失败时,最终抛出
AuthenticationException
3. Maven 依赖
首先添加 Spring Security 核心依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
非 Spring Boot 项目需显式指定版本:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.1.5</version>
</dependency>
4. 自定义认证提供者
通过实现 AuthenticationProvider
接口创建自定义认证逻辑:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
String username = auth.getName();
String password = auth.getCredentials()
.toString();
// 实际项目中这里应该调用外部系统验证
if ("externaluser".equals(username) && "pass".equals(password)) {
return new UsernamePasswordAuthenticationToken
(username, password, Collections.emptyList());
} else {
throw new
BadCredentialsException("External system authentication failed");
}
}
@Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
✅ 实现要点:
authenticate()
方法包含核心认证逻辑- 认证成功返回填充完整的
Authentication
对象 - 认证失败抛出
AuthenticationException
异常 supports()
方法声明支持的认证类型
5. 多认证提供者配置
5.1 Java 配置方式
在安全配置类中组合多个认证提供者:
@Configuration
@EnableWebSecurity
public class MultipleAuthProvidersSecurityConfig {
@Autowired
CustomAuthenticationProvider customAuthProvider;
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
// 添加自定义认证提供者
authBuilder.authenticationProvider(customAuthProvider);
// 添加内存认证提供者
authBuilder.inMemoryAuthentication()
.withUser("memuser")
.password(passwordEncoder().encode("pass"))
.roles("USER");
return authBuilder.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authManager, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.requestMatchers(PathRequest.toH2Console()).authenticated()
.requestMatchers(mvcMatcherBuilder.pattern("/api/**")).authenticated()
)
.authenticationManager(authManager);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5.2 XML 配置方式
传统 XML 配置方案:
<security:authentication-manager>
<!-- 内存认证提供者 -->
<security:authentication-provider>
<security:user-service>
<security:user name="memuser" password="pass"
authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
<!-- 自定义认证提供者 -->
<security:authentication-provider
ref="customAuthenticationProvider" />
</security:authentication-manager>
<security:http>
<security:http-basic />
<security:intercept-url pattern="/api/**"
access="isAuthenticated()" />
</security:http>
6. 应用接口
创建受保护的 REST 接口进行测试:
@RestController
public class MultipleAuthController {
@GetMapping("/api/ping")
public String getPing() {
return "OK";
}
}
该接口需要通过任一认证提供者验证才能访问。
7. 测试验证
编写测试用例验证多认证机制:
@Autowired
private TestRestTemplate restTemplate;
@Test
public void givenMemUsers_whenGetPingWithValidUser_thenOk() {
ResponseEntity<String> result
= makeRestCallToGetPing("memuser", "pass");
assertThat(result.getStatusCode().value()).isEqualTo(200);
assertThat(result.getBody()).isEqualTo("OK");
}
@Test
public void givenExternalUsers_whenGetPingWithValidUser_thenOK() {
ResponseEntity<String> result
= makeRestCallToGetPing("externaluser", "pass");
assertThat(result.getStatusCode().value()).isEqualTo(200);
assertThat(result.getBody()).isEqualTo("OK");
}
@Test
public void givenAuthProviders_whenGetPingWithNoCred_then401() {
ResponseEntity<String> result = makeRestCallToGetPing();
assertThat(result.getStatusCodeValue()).isEqualTo(401);
}
@Test
public void givenAuthProviders_whenGetPingWithBadCred_then401() {
ResponseEntity<String> result
= makeRestCallToGetPing("user", "bad_password");
assertThat(result.getStatusCode().value()).isEqualTo(401);
}
private ResponseEntity<String>
makeRestCallToGetPing(String username, String password) {
return restTemplate.withBasicAuth(username, password)
.getForEntity("/api/ping", String.class, Collections.emptyMap());
}
private ResponseEntity<String> makeRestCallToGetPing() {
return restTemplate
.getForEntity("/api/ping", String.class, Collections.emptyMap());
}
测试覆盖场景: ✅ 内存用户认证成功 ✅ 外部系统用户认证成功 ❌ 无凭证访问被拒绝 ❌ 错误凭证访问被拒绝
8. 总结
本文演示了 Spring Security 中多认证提供者的配置方法,通过组合自定义认证提供者和内存认证提供者,实现了灵活的多源认证方案。实际开发中这种模式常用于:
- 新旧认证系统共存场景
- 多种用户类型(如内部员工/外部客户)分别认证
- 主备认证源切换
完整实现代码可在 GitHub 获取。