1. 简介

本文将详细介绍如何使用纯Java通过LDAP(轻量级目录访问协议)对用户进行身份认证,并重点讲解如何查找用户的专有名称(DN)。这是关键步骤,因为LDAP认证必须使用DN。

我们将利用Java命名和目录接口(JNDI)的目录服务访问能力来完成搜索和认证操作。首先简要介绍LDAP和JNDI的核心概念,然后深入探讨如何通过JNDI API实现LDAP认证。

2. 什么是LDAP?

LDAP定义了客户端向目录服务发送请求并接收响应的标准协议。使用该协议的目录服务称为LDAP服务器。

LDAP服务器提供的数据基于X.500标准的信息模型存储,这是一套电子目录服务的计算机网络标准。

3. 什么是JNDI?

JNDI为应用程序提供了发现和访问命名及目录服务的标准API。其核心目标是为应用程序提供访问本地或网络组件及资源的统一方式。

命名服务是JNDI的基础能力,它通过分层命名空间提供按名称访问服务、数据或对象的单点入口。这些本地或网络资源的名称由托管命名服务的服务器配置。

通过JNDI的命名服务接口可访问LDAP等目录服务,因为目录服务本质是命名服务的特殊形式:每个命名条目可关联属性列表。

每个目录条目除属性外,还可能拥有一个或多个子条目,形成层次化结构。在JNDI中,子条目表示为其父上下文的子上下文。

JNDI API的核心优势在于其底层服务提供者实现的独立性(如LDAP)。因此无需使用特定协议API即可通过JNDI访问LDAP目录服务。

JNDI作为Java SE平台的一部分,无需外部库。作为Java EE核心技术,它广泛应用于企业级应用开发。

4. 使用JNDI进行LDAP认证的核心概念

在分析示例代码前,先了解JNDI API进行LDAP认证的基础知识:

  1. 连接LDAP服务器需创建InitialDirContext对象,需通过Hashtable向构造函数传入环境属性进行配置
  2. Hashtable中设置待认证用户的DN和密码:
    • DN赋值给Context.SECURITY_PRINCIPAL
    • 密码赋值给Context.SECURITY_CREDENTIALS
  3. InitialDirContext实现了核心目录服务接口DirContext,通过该接口可执行:
    • 名称和属性绑定操作
    • 目录条目搜索
  4. JNDI返回对象与其底层LDAP条目保持相同的名称和属性,因此可使用名称和属性作为搜索条件
  5. 通过Attributes接口可检查目录条目的属性,通过Attribute接口可检查具体属性值

5. 当用户DN未知时如何处理?

有时无法直接获取用户DN进行认证。解决步骤:

  1. 使用管理员凭证创建InitialDirContext
  2. 通过该上下文在目录服务器中搜索目标用户并获取其DN
  3. 使用获取的DN和用户凭证创建新的InitialDirContext进行认证

接下来通过示例代码演示完整流程。

6. 示例代码

示例使用ApacheDS嵌入式目录服务器(基于Java的LDAP服务器,专为单元测试设计)。

6.1 配置嵌入式ApacheDS服务器

添加Maven依赖:

<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-test-framework</artifactId>
    <version>2.0.0.AM26</version>
    <scope>test</scope>
</dependency>

创建JUnit 4测试类(注:ApacheDS当前仅兼容JUnit 4):

public class LdapAuthTest extends AbstractLdapTestUnit {
    // 测试代码将添加于此
}

在类声明上方添加ApacheDS注解配置服务器(完整参数见完整示例)。

users.ldif文件加入类路径,使服务器在运行时加载LDIF格式的目录条目(包含用户Joe Simms的记录)。

6.2 用户认证

认证用户Joe Simms需创建InitialDirContext对象,使用其DN和密码建立连接:

Hashtable<String, String> environment = new Hashtable<>();

environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
environment.put(Context.PROVIDER_URL, "ldap://localhost:10389");
environment.put(Context.SECURITY_AUTHENTICATION, "simple");
environment.put(Context.SECURITY_PRINCIPAL, "cn=Joe Simms,ou=Users,dc=baeldung,dc=com");
environment.put(Context.SECURITY_CREDENTIALS, "12345");

认证方法实现:

DirContext context = new InitialDirContext(environment);
context.close();

验证认证成功:

assertThatCode(() -> authenticateUser(environment)).doesNotThrowAnyException();

6.3 处理认证失败

使用错误密码触发认证失败:

environment.put(Context.SECURITY_CREDENTIALS, "wrongpassword");

验证异常抛出:

assertThatExceptionOfType(AuthenticationException.class)
    .isThrownBy(() -> authenticateUser(environment));

6.4 通过管理员查询用户DN

当用户DN未知时,需通过管理员上下文查询:

Hashtable<String, String> env = new Hashtable<>();

env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:10389");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system");
env.put(Context.SECURITY_CREDENTIALS, "secret");

DirContext adminContext = new InitialDirContext(env);

基于用户通用名(CN)定义搜索过滤器:

String filter = "(&(objectClass=person)(cn=Joe Simms))";

配置搜索控制参数:

String[] attrIDs = { "cn" };
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(attrIDs);
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

执行搜索并提取DN:

NamingEnumeration<SearchResult> searchResults 
    = adminContext.search("dc=baeldung,dc=com", filter, searchControls);

String distinguishedName = null;
if (searchResults.hasMore()) {
    SearchResult result = searchResults.next();
    distinguishedName = result.getNameInNamespace();
    assertThat(distinguishedName)
        .isEqualTo("cn=Joe Simms,ou=Users,dc=baeldung,dc=com");
}

6.5 使用查询到的DN认证用户

用获取的DN替换管理员凭证:

env.put(Context.SECURITY_PRINCIPAL, distinguishedName);
env.put(Context.SECURITY_CREDENTIALS, "12345");

执行用户认证:

assertThatCode(() -> authenticateUser(env)).doesNotThrowAnyException();

清理资源:

adminContext.close();

7. 总结

本文详细介绍了如何使用JNDI通过用户DN和密码实现LDAP认证,并演示了当用户DN未知时的查询方法。完整示例代码可在GitHub仓库获取。

关键要点总结:

  • LDAP认证必须使用用户专有名称(DN)
  • JNDI提供协议无关的目录服务访问能力
  • 认证流程分两步:管理员查询DN → 用户凭证认证
  • 嵌入式ApacheDS适合单元测试场景
  • 务必记得关闭DirContext释放资源

原始标题:LDAP Authentication Using Pure Java | Baeldung