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 获取。


原始标题:Multiple Authentication Providers in Spring Security