Spring LDAP查询Active Directory无法返回用户记录的问题排查
我正在编写Spring LDAP程序,用于查询Active Directory中的用户记录,但无法返回结果。相同的查询在Apache Directory Studio中能正常执行并返回我的用户ID,但Spring LDAP运行时抛出以下错误:
org.springframework.ldap.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-0310028C, problem 2001 (NO_OBJECT), data 0, best match of:
'CN=Users,DC=myserver,DC=myschool,DC=edu'
]
理论上应该能找到用户记录,想知道哪里操作有误,以及Spring LDAP无法正常工作的原因。
技术细节
项目是通过Spring Initializr创建的Groovy项目,要求Java 17环境。
Gradle配置
build.gradle相关内容:
plugins { id 'groovy' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' } group = 'edu.myschool' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } repositories { mavenCentral() } ext { set('springShellVersion', "3.3.1") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-ldap' implementation 'org.apache.groovy:groovy' implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.shell:spring-shell-starter' } dependencyManagement { imports { mavenBom "org.springframework.shell:spring-shell-dependencies:${springShellVersion}" } }
Java测试类
为了排除Groovy调用差异,编写了Java测试类(位于Groovy源码目录),核心逻辑在queryForUser方法:
package edu.sunyjcc.testbed; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.io.StringWriter; import java.io.BufferedWriter; import java.io.IOException; import javax.naming.directory.SearchControls; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.AttributesMapper; import javax.naming.directory.Attributes; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.context.annotation.PropertySource; /** * This is a minimal example that should demonstrate my problem. */ @Component @PropertySource("classpath:application.properties") public class HelpTicketExample { protected int scope = SearchControls.SUBTREE_SCOPE; protected boolean ignorePartialResultException = true; @Value("${server.urls}") String url; @Value("${manager.userdn}") String userDn; @Value("${manager.password}") String password; @Value("${username}") protected String usernameAttr; @Value("${password}") protected String passwordAttr; // protected LdapTemplate ldapTemplate; // protected LdapContextSource ldapContextSource; @Value("${basedn}") protected String searchBase; /** Work around the lack of a println in StringWriter */ interface Printer { String println(String str) throws IOException; } String filter = "(&(objectClass=person)(sAMAccountName=myusername))"; /** Print out the configuration information */ public void showConfig(Printer p) throws IOException { p.println("**********************************************************************"); p.println("* usernameAttr: " + usernameAttr); p.println("* searchBase: " + searchBase); p.println("* filter: " + filter); p.println("**********************************************************************"); } public Object queryForUser(String username) throws IOException { StringWriter s = new StringWriter(); BufferedWriter b = new BufferedWriter(s); Printer p = (text) -> { b.write(text); b.newLine(); return text; }; ////////////////////////////////////////////////////////////////////// // Get to work ////////////////////////////////////////////////////////////////////// p.println("************************************************************" ); p.println("queryForUser(" + username + ")"); b.newLine(); showConfig(p); ////////////////////////////////////////////////////////////////////// LdapContextSource contextSource = new LdapContextSource(); contextSource.setUrl(url); contextSource.setBase(searchBase); contextSource.setUserDn(userDn); contextSource.setPassword(password); contextSource.afterPropertiesSet(); ////////////////////////////////////////////////////////////////////// AttributesMapper<String> mapper = new AttributesMapper() { public String mapFromAttributes(Attributes attributes) { return "Attributes found"; } }; ////////////////////////////////////////////////////////////////////// LdapTemplate ldapTemplate = new LdapTemplate(contextSource); ldapTemplate.setIgnorePartialResultException(true); try { ldapTemplate.afterPropertiesSet(); @SuppressWarnings("unchecked") List<String> results = ldapTemplate.search(searchBase, filter, scope, mapper); } catch (Exception e) { p.println(e.toString()); } ////////////////////////////////////////////////////////////////////// b.close(); return s.toString(); } public HelpTicketExample() { } }
程序输出
********************************************************** queryForUser(myusername) ********************************************************************* # usernameAttr: sAMAccountName # searchBase: cn=Users,dc=myserver,dc=myschool,dc=edu # filter: (&(objectClass=person)(sAMAccountName=myusername)) ********************************************************************* org.springframework.ldap.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-0310028C, problem 2001 (NO_OBJECT), data 0, best match of: 'CN=Users,DC=myserver,DC=myschool,DC=edu' ]
问题排查与解决
核心问题:重复设置搜索基准(Search Base)
代码中存在一个关键错误:同时给LdapContextSource设置了searchBase,又在ldapTemplate.search()方法中再次传入相同的searchBase参数。
当你调用LdapContextSource.setBase(searchBase)时,Spring LDAP会自动将这个基准DN作为所有LDAP操作的根路径。此时如果在search()方法中再次传入同一个searchBase,相当于要求LDAP服务器在CN=Users,DC=myserver,DC=myschool,DC=edu下面查找另一个完全相同的DN,这显然不存在,因此抛出NO_OBJECT(错误码32)异常。
修复方案
有两种可选修复方式:
- 移除
LdapContextSource.setBase(searchBase),仅在search()方法中传入searchBase参数。 - 保留
LdapContextSource.setBase(searchBase),在search()方法中传入空字符串""或null作为搜索基准,此时LDAP会使用ContextSource中设置的路径作为根。
修改后的queryForUser方法关键代码示例(方案2):
// 保留ContextSource的base设置 LdapContextSource contextSource = new LdapContextSource(); contextSource.setUrl(url); contextSource.setBase(searchBase); // 保留这行 contextSource.setUserDn(userDn); contextSource.setPassword(password); contextSource.afterPropertiesSet(); // 搜索时传入空字符串作为基准 List<String> results = ldapTemplate.search("", filter, scope, mapper);
其他可能的检查点
- 确认
manager.userdn格式正确:Active Directory中绑定用户的DN通常为CN=管理员名称,CN=Users,DC=myserver,DC=myschool,DC=edu,检查是否存在拼写错误。 - 验证LDAP连接URL:确保包含正确端口(默认389,SSL用636),例如
ldap://myserver.myschool.edu:389。 - 确认用户
sAMAccountName准确:虽然Active Directory默认大小写不敏感,但部分环境可能存在差异,需确保与实际值一致。
内容的提问来源于stack exchange,提问作者Big Ed

