You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring Security中创建条件式AuthenticationProvider?

解决Spring Security多认证源下的密码编码器适配问题

嘿,咱们一步步来搞定这个问题。你现在已经有了基于Spring Security的认证配置:数据库里存的是BCrypt加密后的密码,通过自定义UserDetailsService查询用户信息。现在要新增一个外部认证源,这个源返回的是明文密码(而且你没法修改它的逻辑),要求当数据库里找不到用户时,就用这个外部源做明文密码验证,同时在这个场景下禁用BCrypt编码器。

核心思路

Spring Security允许配置多个AuthenticationProvider,每个Provider可以对应不同的认证逻辑和密码编码器。我们可以这么做:

  • 保留原来基于UserDetailsService的数据库认证Provider,继续用BCrypt编码器
  • 新增一个自定义的AuthenticationProvider,专门处理外部明文认证的逻辑,用明文匹配的编码器
  • 把这两个Provider按顺序配置到AuthenticationManager里,Spring Security会依次尝试认证,直到成功或者所有Provider都失败

具体实现步骤

1. 自定义明文密码编码器(替代过时的NoOpPasswordEncoder)

官方的NoOpPasswordEncoder已经被标记为过时了,咱们自己写个简单的明文匹配编码器更稳妥:

import org.springframework.security.crypto.password.PasswordEncoder;

public class PlainTextPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        // 明文不需要加密,直接返回原字符串就行
        return rawPassword.toString();
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // 直接对比输入的明文和外部源返回的明文密码
        return rawPassword.toString().equals(encodedPassword);
    }
}

2. 定义外部认证服务接口

先定义一个外部认证服务的接口,假设它能根据用户名返回对应的明文密码(返回null表示用户不存在):

// 根据你的实际外部服务逻辑调整
public interface ExternalAuthService {
    String getPlainPasswordByUsername(String username);
}

3. 实现外部认证的AuthenticationProvider

接下来写一个自定义的AuthenticationProvider,专门处理外部认证源的逻辑:

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

public class ExternalAuthProvider implements AuthenticationProvider {

    private final ExternalAuthService externalAuthService;
    private final PasswordEncoder plainTextPasswordEncoder;

    // 通过构造器注入外部服务和明文编码器
    public ExternalAuthProvider(ExternalAuthService externalAuthService, PasswordEncoder plainTextPasswordEncoder) {
        this.externalAuthService = externalAuthService;
        this.plainTextPasswordEncoder = plainTextPasswordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String rawPassword = authentication.getCredentials().toString();

        // 调用外部服务获取明文密码
        String externalPlainPwd = externalAuthService.getPlainPasswordByUsername(username);
        if (externalPlainPwd == null) {
            // 外部源也找不到这个用户,抛出异常让Spring尝试下一个Provider
            throw new BadCredentialsException("External auth failed: user not found");
        }

        // 验证明文密码是否匹配
        if (!plainTextPasswordEncoder.matches(rawPassword, externalPlainPwd)) {
            throw new BadCredentialsException("External auth failed: incorrect password");
        }

        // 构造认证成功的UserDetails,权限可根据实际需求调整
        UserDetails userDetails = User.withUsername(username)
                .password(externalPlainPwd)
                .authorities("ROLE_EXTERNAL_USER")
                .build();

        // 返回认证成功的Token
        return new UsernamePasswordAuthenticationToken(userDetails, rawPassword, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 只处理用户名密码类型的认证请求
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

4. 修改SecurityConfiguration,配置两个AuthenticationProvider

原来的配置是直接通过auth.userDetailsService(...)绑定编码器,现在我们要手动配置两个Provider,确保数据库认证优先:

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private ExternalAuthService externalAuthService;

    // 注册BCrypt编码器Bean
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 注册明文编码器Bean
    @Bean
    public PasswordEncoder plainTextPasswordEncoder() {
        return new PlainTextPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 第一个Provider:数据库认证,用BCrypt编码器
        auth.authenticationProvider(databaseAuthProvider());
        // 第二个Provider:外部明文认证,用自定义的明文编码器
        auth.authenticationProvider(externalAuthProvider());
    }

    // 构建数据库认证的Provider
    @Bean
    public AuthenticationProvider databaseAuthProvider() {
        var daoProvider = new org.springframework.security.authentication.dao.DaoAuthenticationProvider();
        daoProvider.setUserDetailsService(userDetailsService);
        daoProvider.setPasswordEncoder(bCryptPasswordEncoder());
        return daoProvider;
    }

    // 构建外部认证的Provider
    @Bean
    public AuthenticationProvider externalAuthProvider() {
        return new ExternalAuthProvider(externalAuthService, plainTextPasswordEncoder());
    }

    // 如果需要在其他地方注入AuthenticationManager,需重写此方法
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

关键细节说明

  • Spring Security会按配置顺序依次尝试每个Provider,只有当前一个Provider抛出AuthenticationException时,才会触发下一个Provider。数据库认证优先执行,当数据库找不到用户时,DaoAuthenticationProvider会抛出UsernameNotFoundException,这时就轮到外部认证Provider工作。
  • 明文编码器只在外部认证场景下使用,和数据库认证的BCrypt编码器完全隔离,不会互相干扰。
  • 你可以根据实际需求调整Provider的顺序,但通常优先使用本地数据库的用户认证。

内容的提问来源于stack exchange,提问作者membersound

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 06:43:05