使用多个AuthenticationProviders时如何统一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完全控制用户对象生成:
- 实现自定义
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); } } }
- 修改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

