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

使用多个AuthenticationProviders时如何统一User对象类型?

统一Spring Security双认证提供者的Principal为自定义User类

问题描述

你当前的Spring Security配置了JDBC和LDAP两个认证提供者:

@Bean
public AuthenticationManager authenticationManager(final DataSource dataSource, final ContextSource contextSource,
    final ObjectPostProcessor<Object> objectPostProcessor) throws Exception {
    final AuthenticationManagerBuilder auth = new AuthenticationManagerBuilder(objectPostProcessor);
    auth.ldapAuthentication()
        .contextSource((BaseLdapPathContextSource) contextSource)
        .userSearchFilter("(| (sAMAccountName={0}))");
    auth.jdbcAuthentication()
        .passwordEncoder(new BCryptPasswordEncoder())
        .dataSource(dataSource)
        .usersByUsernameQuery("select username, password, enabled from Users where lower(username) = lower(?)")
        .authoritiesByUsernameQuery("select username, role from UserRoles where lower(username) = lower(?)");
    return auth.build();
}

登录后通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取用户数据时,JDBC认证返回org.springframework.security.core.userdetails.User对象,LDAP认证返回org.springframework.security.ldap.userdetails.LdapUserDetailsImpl对象。你希望将两者统一为自定义User类(包含全名、头像等额外字段),同时避免在首次访问时替换Principal这种不规范且易出错的方式。

你已发现LDAP认证可通过userDetailsContextMapper()方法自定义,但不清楚JDBC认证的等效配置方式,希望了解如何在JDBC认证中实现自定义User对象的生成。


解决方案

一、定义自定义User类

先实现UserDetails接口,创建包含自定义字段的用户类:

public class CustomUser implements UserDetails {
    private final String username;
    private final String password;
    private final boolean enabled;
    private final Collection<? extends GrantedAuthority> authorities;
    // 自定义业务字段
    private final String fullName;
    private final String avatarUrl;

    // 全参构造方法
    public CustomUser(String username, String password, boolean enabled,
                      Collection<? extends GrantedAuthority> authorities,
                      String fullName, String avatarUrl) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.authorities = authorities;
        this.fullName = fullName;
        this.avatarUrl = avatarUrl;
    }

    // UserDetails接口方法实现
    @Override
    public String getUsername() { return username; }
    @Override
    public String getPassword() { return password; }
    @Override
    public boolean isEnabled() { return enabled; }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
    // 按需实现其他UserDetails方法(如isAccountNonExpired等)

    // 自定义字段getter
    public String getFullName() { return fullName; }
    public String getAvatarUrl() { return avatarUrl; }
}

二、LDAP认证的自定义映射

使用userDetailsContextMapper()方法,将LDAP返回的用户信息映射到CustomUser:

auth.ldapAuthentication()
    .contextSource((BaseLdapPathContextSource) contextSource)
    .userSearchFilter("(| (sAMAccountName={0}))")
    .userDetailsContextMapper(new UserDetailsContextMapper() {
        @Override
        public UserDetails mapUserFromContext(DirContextOperations ctx, String username, 
                                             Collection<? extends GrantedAuthority> authorities) {
            // 从LDAP上下文提取自定义字段(示例取cn作为全名)
            String fullName = ctx.getStringAttribute("cn");
            String avatarUrl = ""; // 根据业务逻辑填充,比如从LDAP自定义属性或外部系统获取
            
            return new CustomUser(
                username,
                "", // LDAP认证无需本地存储密码,留空或按需处理
                true, // 可从LDAP属性判断账号启用状态
                authorities,
                fullName,
                avatarUrl
            );
        }

        @Override
        public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
            // 无需写回LDAP则留空
        }
    });

三、JDBC认证的自定义实现

通过自定义UserDetailsService完全控制用户对象生成:

  1. 实现自定义UserDetailsService:
@Service
public class CustomJdbcUserDetailsService implements UserDetailsService {
    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;

    public CustomJdbcUserDetailsService(DataSource dataSource, PasswordEncoder passwordEncoder) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户基础信息+自定义字段
        String userSql = "select username, password, enabled, full_name, avatar_url from Users where lower(username) = lower(?)";
        try (Connection conn = dataSource.getConnection()) {
            PreparedStatement userStmt = conn.prepareStatement(userSql);
            userStmt.setString(1, username);
            ResultSet userRs = userStmt.executeQuery();
            
            if (!userRs.next()) {
                throw new UsernameNotFoundException("用户不存在: " + username);
            }

            // 提取用户信息
            String dbUsername = userRs.getString("username");
            String password = userRs.getString("password");
            boolean enabled = userRs.getBoolean("enabled");
            String fullName = userRs.getString("full_name");
            String avatarUrl = userRs.getString("avatar_url");

            // 查询用户权限
            String authSql = "select role from UserRoles where lower(username) = lower(?)";
            PreparedStatement authStmt = conn.prepareStatement(authSql);
            authStmt.setString(1, username);
            ResultSet authRs = authStmt.executeQuery();
            
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            while (authRs.next()) {
                authorities.add(new SimpleGrantedAuthority(authRs.getString("role")));
            }

            return new CustomUser(dbUsername, password, enabled, authorities, fullName, avatarUrl);
        } catch (SQLException e) {
            throw new RuntimeException("数据库查询失败", e);
        }
    }
}
  1. 修改AuthenticationManager配置,替换默认JDBC认证逻辑:
@Autowired
private CustomJdbcUserDetailsService customJdbcUserDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;

@Bean
public AuthenticationManager authenticationManager(final DataSource dataSource, final ContextSource contextSource,
    final ObjectPostProcessor<Object> objectPostProcessor) throws Exception {
    final AuthenticationManagerBuilder auth = new AuthenticationManagerBuilder(objectPostProcessor);
    
    // LDAP认证配置(保留上面的自定义映射)
    auth.ldapAuthentication()
        .contextSource((BaseLdapPathContextSource) contextSource)
        .userSearchFilter("(| (sAMAccountName={0}))")
        .userDetailsContextMapper(new UserDetailsContextMapper() {
            // 实现同前
        });

    // JDBC认证使用自定义UserDetailsService
    auth.userDetailsService(customJdbcUserDetailsService)
        .passwordEncoder(passwordEncoder);

    return auth.build();
}

可选优化:如果需要保留JDBC用户管理的默认逻辑(如创建、修改用户),可以继承JdbcUserDetailsManager并重写loadUserByUsername()方法,无需完全实现UserDetailsService。

四、方案优势

通过自定义UserDetailsContextMapper(LDAP)和UserDetailsService(JDBC),认证过程会直接生成CustomUser作为Principal,无需后续替换操作,完全符合Spring Security扩展规范,从根源避免了跳过处理代码的风险。


内容的提问来源于stack exchange,提问作者Sebastian Lütge

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 10:48:22