多LDAP服务下LdapAuthenticationProvider单次错密触发账号锁定问题
问题说明
使用spring-security 5.4.2对接LDAP认证时,当配置了多个LDAP服务器地址时,仅输错一次密码就会触发账号锁定,该问题在JDK 8u252版本不会复现,在JDK 8u301及更高版本可以稳定复现。
抓包确认高版本JDK会向所有配置的LDAP服务器都发送密码校验请求,错误次数叠加超过Bad-Pwd-Count阈值就会锁定账号。
复现步骤
- 启动测试LDAP服务:
docker pull rroemhild/test-openldap docker run --rm -p 10389:10389 -p 10636:10636 rroemhild/test-openldap tcpdump -i docker0 port 10389 or port 10636
- 运行测试代码:
@RunWith(SpringJUnit4ClassRunner.class) @EnableWebSecurity @WebAppConfiguration @EnableWebMvc @ContextConfiguration(loader = AnnotationConfigWebContextLoader.class) public class LdapAuthOpenLdapTest { @Configuration @EnableWebSecurity @WebAppConfiguration @EnableWebMvc static class LdapSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) { auth.authenticationProvider(ldapAuthProvider()); } @Bean DefaultSpringSecurityContextSource ldapServer() { DefaultSpringSecurityContextSource result = new DefaultSpringSecurityContextSource( "ldap://localhost:10389 ldaps://localhost:10636"); result.setUserDn("cn=admin,dc=planetexpress,dc=com"); result.setPassword("GoodNewsEveryone"); return result; } @Bean FilterBasedLdapUserSearch userSearch() { return new FilterBasedLdapUserSearch("dc=planetexpress,dc=com", "(&(uid={0})(objectClass=inetOrgPerson))", ldapServer()); } @Bean LdapAuthenticationProvider ldapAuthProvider() { BindAuthenticator bindAuthenticator = new BindAuthenticator( ldapServer()); bindAuthenticator.setUserSearch(userSearch()); DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator( ldapServer(), "ou=people,dc=planetexpress,dc=com"); authoritiesPopulator.setGroupRoleAttribute("cn"); authoritiesPopulator.setRolePrefix("ROLE_"); authoritiesPopulator.setConvertToUpperCase(true); return new LdapAuthenticationProvider(bindAuthenticator, authoritiesPopulator); } } @Autowired LdapAuthenticationProvider ldapAuthProvider; @Test public void test() { Exception ex = null; try { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "professor", "professor1"); Authentication auth = ldapAuthProvider.authenticate(token); System.out.println(auth); } catch (Exception e) { e.printStackTrace(); } assertThat(ex, is(nullValue())); } }
问题解答
哪一种行为符合预期?
JDK 8u252的行为才符合设计预期。
该问题是高版本JDK引入的LDAP实现逻辑bug:原本LDAP客户端的重试逻辑设计为,仅当LDAP服务器连接失败、无响应这类网络/服务不可用场景时,才会尝试下一个配置的LDAP节点。密码错误属于业务层面的认证失败,已经明确得到了服务器的拒绝响应,不应该触发重试逻辑。JDK 8u271之后的版本错误将认证失败也纳入了重试触发条件,才会遍历所有配置的LDAP服务器发送认证请求,导致错误次数叠加。
如何避免高版本JDK下的账号锁定问题?
可按优先级选择以下任意方案解决:
- 方案1(改动最小,无需改代码):在应用启动参数中添加JVM系统属性禁用非必要的LDAP重试:
配置后只会在首个LDAP节点连接失败时才尝试下一个节点,密码错误会直接返回,不会触发后续节点的认证请求。-Dcom.sun.jndi.ldap.connect.retry=false - 方案2(原生修复):升级JDK到已修复该bug的版本。JDK 8u371及以上的JDK 8更新版本,或者11、17等更高LTS版本的JDK都已经修复了这个逻辑问题,升级后即可原生解决。
- 方案3(代码层面兼容):修改Spring Security LDAP配置,自行实现LDAP节点选择逻辑,每次认证仅向单个可用的LDAP节点发起请求,认证失败后直接终止流程,不依赖JDK的内置重试逻辑。
内容的提问来源于stack exchange,提问作者cmadsen
相关产品推荐
相关产品推荐

