Spring LDAP 使构建使用轻量级目录访问协议的基于 Spring 的应用程序变得更加容易。

本文档的副本可以供您自己使用和分发给其他人,前提是您不对此类副本收取任何费用,并且进一步前提是每份副本都包含本版权声明,无论是印刷版还是电子版。

1.前言

Java 命名和目录接口 (JNDI) 之于 LDAP 编程就像 Java 数据库连接 (JDBC) 之于 SQL 编程。JDBC 和 JNDI/LDAP(Java LDAP)之间有几个相似之处。尽管是两种完全不同的 API,各有优缺点,但它们都有一些不那么讨人喜欢的特征:

  • 他们需要大量的管道代码,即使是执行最简单的任务。

  • 无论发生什么,所有资源都需要正确关闭。

  • 异常处理很困难。

这些点通常会导致 API 的常见用例中出现大量代码重复。众所周知,代码重复是最糟糕的“代码异味”之一。总而言之,归结为:Java 中的 JDBC 和 LDAP 编程都非常枯燥和重复。

Spring JDBC 是 Spring Framework 的核心组件,为简化 SQL 编程提供了出色的实用程序。我们需要一个类似的 Java LDAP 编程框架。

2. 简介

本节相对快速地介绍了 Spring LDAP。它包括以下内容:

2.1。概述

Spring LDAP 旨在简化 Java 中的 LDAP 编程。该库提供的一些功能包括:

  • JdbcTemplate- 样式模板简化到 LDAP 编程。

  • JPA 或 Hibernate 样式的基于注释的对象和目录映射。

  • Spring Data 存储库支持,包括对 QueryDSL 的支持。

  • 用于简化构建 LDAP 查询和专有名称的实用程序。

  • 正确的 LDAP 连接池。

  • 客户端 LDAP 补偿事务支持。

2.2. 传统 Java LDAP 与LdapTemplate

考虑一种方法,该方法应在一些存储空间中搜索所有人并在列表中返回他们的姓名。通过使用 JDBC,我们将创建连接并使用语句运行查询。然后,我们将遍历结果集并检索我们想要的,并将其添加到列表中。

使用 JNDI 处理 LDAP 数据库,我们将创建一个上下文并使用搜索过滤器执行搜索。然后,我们将遍历生成的命名枚举,检索我们想要的属性,并将其添加到列表中。

在 Java LDAP 中实现这种人名搜索方法的传统方式类似于下一个示例。请注意标记为粗体的代码- 这是实际执行与方法的业务目的相关的任务的代码。剩下的就是水管了。

package com.example.repository;

public class TraditionalPersonRepoImpl implements PersonRepo {
   public List<String> getAllPersonNames() {
      Hashtable env = new Hashtable();
      env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
      env.put(Context.PROVIDER_URL, "ldap://localhost:389/dc=example,dc=com");

      DirContext ctx;
      try {
         ctx = new InitialDirContext(env);
      } catch (NamingException e) {
         throw new RuntimeException(e);
      }

      List<String> list = new LinkedList<String>();
      NamingEnumeration results = null;
      try {
         SearchControls controls = new SearchControls();
         controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
         results = ctx.search("", "(objectclass=person)", controls);

         while (results.hasMore()) {
            SearchResult searchResult = (SearchResult) results.next();
            Attributes attributes = searchResult.getAttributes();
            Attribute attr = attributes.get("cn");
            String cn = attr.get().toString();
            list.add(cn);
         }
      } catch (NameNotFoundException e) {
         // The base context was not found.
         // Just clean up and exit.
      } catch (NamingException e) {
         throw new RuntimeException(e);
      } finally {
         if (results != null) {
            try {
               results.close();
            } catch (Exception e) {
               // Never mind this.
            }
         }
         if (ctx != null) {
            try {
               ctx.close();
            } catch (Exception e) {
               // Never mind this.
            }
         }
      }
      return list;
   }
}

通过使用 Spring LDAPAttributesMapperLdapTemplate类,我们可以使用以下代码获得完全相同的功能:

package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;

   public void setLdapTemplate(LdapTemplate ldapTemplate) {
      this.ldapTemplate = ldapTemplate;
   }

   public List<String> getAllPersonNames() {
      return ldapTemplate.search(
         query().where("objectclass").is("person"),
         new AttributesMapper<String>() {
            public String mapFromAttributes(Attributes attrs)
               throws NamingException {
               return attrs.get("cn").get().toString();
            }
         });
   }
}

样板代码的数量明显少于传统示例。LdapTemplatesearch 方法确保创建了一个实例DirContext,执行搜索,使用给定的将属性映射到字符串,AttributesMapper将字符串收集到内部列表中,最后返回列表。它还确保NamingEnumerationandDirContext正确关闭并处理可能发生的任何异常。

当然,这是一个 Spring Framework 的子项目,我们使用 Spring 来配置我们的应用程序,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:ldap="http://www.springframework.org/schema/ldap"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/ldap https://www.springframework.org/schema/ldap/spring-ldap.xsd">

   <ldap:context-source
          url="ldap://localhost:389"
          base="dc=example,dc=com"
          username="cn=Manager"
          password="secret" />

   <ldap:ldap-template id="ldapTemplate" />

   <bean id="personRepo" class="com.example.repo.PersonRepoImpl">
      <property name="ldapTemplate" ref="ldapTemplate" />
   </bean>
</beans>
要使用自定义 XML 命名空间来配置 Spring LDAP 组件,您需要在 XML 声明中包含对该命名空间的引用,如前面的示例中所示。

2.3. 2.2 中的新功能

有关 2.2 的完整详细信息,请参阅2.2.0.RC1的更改日志。Spring LDAP 2.2 的亮点如下:

  • #415:增加了对 Spring 5 的支持

  • #399 : 嵌入式 UnboundID LDAP 服务器支持

  • #410:为 Commons Pool 2 支持添加了文档

2.4. 2.1 中的新功能

有关 2.1 的完整详细信息,请参阅2.1.0.RC12.1.0的更改日志 Spring LDAP 2.1 的亮点如下。

  • #390:添加了 Spring Data Hopper 支持

  • #351 : 添加了对 commons-pool2 的支持

  • #370 : 在 XML 命名空间中添加了支持属性占位符

  • #392 : 添加文档测试支持

  • #401 : 添加了一个到 assertj 的开关

  • 从 JIRA 迁移到GitHub 问题

  • 添加了 Gitter 聊天

2.5. 2.0 中的新功能

虽然在 2.0 版中对 Spring LDAP API 进行了相当大的现代化改造,但我们已经非常注意尽可能地确保向后兼容性。使用 Spring LDAP 1.3.x 的代码应该在您使用 2.0 库而不做任何修改时编译和运行,除了少数例外。

少数类已被移动到新包中以使一些重要的重构成为可能,这是个例外。移动的类通常不是预期的公共 API 的一部分,迁移过程应该是顺利的。每当升级后找不到 Spring LDAP 类时,您应该在 IDE 中组织导入。

不过,您应该会遇到一些弃用警告,并且还有许多其他 API 改进。尽可能多地使用 2.0 版本的建议是远离不推荐使用的类和方法,并迁移到新的、改进的 API 实用程序。

下面的列表简要描述了 Spring LDAP 2.0 中最重要的变化:

  • Spring LDAP 现在需要 Java 6。仍然支持从 2.0 及更高版本开始的 Spring 版本。

  • 中央 API 已更新为 Java 5+ 特性,例如泛型和可变参数。因此,整个spring-ldap-tiger模块已被弃用,我们鼓励您迁移到使用核心 Spring LDAP 类。核心接口的参数化会导致现有代码出现大量编译警告,我们鼓励您采取适当的措施来消除这些警告。

  • ODM(对象目录映射)功能已移至核心,并且有一些新方法LdapOperations使用LdapTemplate这种自动转换到 ODM 注释类。有关详细信息,请参阅对象目录映射 (ODM)

  • 现在(最终)提供了一个自定义 XML 命名空间来简化 Spring LDAP 的配置。有关详细信息,请参阅配置

  • Spring LDAP 现在提供对 Spring Data Repository 和 QueryDSL 的支持。有关更多信息,请参阅Spring LDAP 存储库

  • Name现在,在 ODM 和 ODM 中,关于可分辨名称相等性,作为属性值的实例得到了正确处理DirContextAdapter。有关详细信息,请参阅作为属性值的DirContextAdapter专有名称和作为属性值ODM 和专有名称

  • DistinguishedName和相关的类已被弃用,取而代之的是标准 Java LdapName。有关库在处理对象时如何提供帮助的信息,请参阅动态构建可分辨名称。LdapName

  • 添加了 Fluent LDAP 查询构建支持。在 Spring LDAP 中使用 LDAP 搜索时,这会带来更愉快的编程体验。有关LDAP 查询构建器支持的更多信息,请参阅构建 LDAP 查询高级 LDAP 查询。

  • 中的旧authenticate方法LdapTemplate已被弃用,取而代之的是一些与对象一起使用并在身份验证失败时抛出异常authenticate的新方法,使用户更容易找出导致身份验证尝试失败的原因。LdapQuery

  • 这些示例已经过完善和更新,以利用 2.0 中的功能。为了提供一个有用的LDAP 用户管理应用程序示例,我们付出了相当多的努力。

2.6. 包装概述

至少,要使用 Spring LDAP,您需要以下内容:

  • spring-ldap-core: Spring LDAP 库

  • spring-core:框架内部使用的其他实用程序类

  • spring-beans: 用于操作 Java bean 的接口和类

  • slf4j: 一个简单的日志外观,内部使用

除了必需的依赖项之外,某些功能还需要以下可选依赖项:

  • spring-data-ldap:用于存储库支持等的基础架构

  • spring-context:如果您的应用程序使用 Spring Application Context 连接起来,则需要。spring-context添加了应用程序对象通过使用一致的 API 获取资源的能力。如果您打算使用BaseLdapPathBeanPostProcessor.

  • spring-tx:如果您计划使用客户端补偿事务支持,则需要。

  • spring-jdbc:如果您计划使用客户端补偿事务支持,则需要。

  • commons-pool:如果您打算使用池功能,则需要。

  • spring-batch:如果您计划将 LDIF 解析功能与 Spring Batch 一起使用,则需要。

spring-data-ldap传递性地添加spring-repository.xsd, 它spring-ldap.xsd使用。因此,Spring LDAP 的 XML 配置支持需要依赖,即使 Spring Data 的功能集未使用。

2.7. 入门

这些示例提供了一些有用的示例,说明如何将 Spring LDAP 用于常见用例。

2.8. 支持

如果您有任何问题,请在Stack Overflow 上使用spring-ldap标签向他们提问。项目网页是https://spring.io/spring-ldap/

2.9。致谢

启动 Spring LDAP 项目的最初工作是由Jayway赞助的。目前该项目的维护由Pivotal提供资金,该公司后来被VMware收购。

感谢Structure101提供的开源许可证,它可以方便地检查项目结构。

3.基本用法

本节介绍使用 Spring LDAP 的基础知识。它包含以下内容:

3.1。搜索和查找使用AttributesMapper

以下示例使用 anAttributesMapper来构建所有人员对象的所有常用名称的列表。

示例 1.AttributesMapper返回单个属性
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;

   public void setLdapTemplate(LdapTemplate ldapTemplate) {
      this.ldapTemplate = ldapTemplate;
   }

   public List<String> getAllPersonNames() {
      return ldapTemplate.search(
         query().where("objectclass").is("person"),
         new AttributesMapper<String>() {
            public String mapFromAttributes(Attributes attrs)
               throws NamingException {
               return (String) attrs.get("cn").get();
            }
         });
   }
}

的内联实现AttributesMapper从对象中获取所需的属性值Attributes并返回它。在内部,LdapTemplate遍历所有找到的AttributesMapper条目,为每个条目调用给定的,并将结果收集到一个列表中。该列表随后由该search方法返回。

请注意,AttributesMapper可以轻松修改实现以返回完整Person对象,如下所示:

示例 2. 返回 Person 对象的 AttributesMapper
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   private class PersonAttributesMapper implements AttributesMapper<Person> {
      public Person mapFromAttributes(Attributes attrs) throws NamingException {
         Person person = new Person();
         person.setFullName((String)attrs.get("cn").get());
         person.setLastName((String)attrs.get("sn").get());
         person.setDescription((String)attrs.get("description").get());
         return person;
      }
   }

   public List<Person> getAllPersons() {
      return ldapTemplate.search(query()
          .where("objectclass").is("person"), new PersonAttributesMapper());
   }
}

LDAP 中的条目由它们的专有名称 (DN) 唯一标识。如果您有条目的 DN,则可以直接检索该条目而无需搜索它。这在 Java LDAP 中称为“查找”。以下示例显示了对Person对象的查找:

示例 3. 生成 Person 对象的查找
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   public Person findPerson(String dn) {
      return ldapTemplate.lookup(dn, new PersonAttributesMapper());
   }
}

前面的示例查找指定的 DN 并将找到的属性传递给提供的属性AttributesMapper ——在本例中,生成一个Person对象。

3.2. 构建 LDAP 查询

LDAP 搜索涉及许多参数,包括:

  • 基本 LDAP 路径:搜索应从 LDAP 树的哪个位置开始。

  • 搜索范围:搜索应在 LDAP 树中多深。

  • 要返回的属性。

  • 搜索过滤器:选择范围内的元素时使用的条件。

Spring LDAP 提供了LdapQueryBuilder一个流畅的 API 来构建 LDAP 查询。

假设您要从基本 DN 开始执行搜索dc=261consulting,dc=com,将返回的属性限制为cnsn,过滤器为(&(objectclass=person)(sn=?)),我们希望?将 替换为lastName参数的值。以下示例显示了如何使用LdapQueryBuilder:

示例 4. 动态构建搜索过滤器
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   public List<String> getPersonNamesByLastName(String lastName) {

      LdapQuery query = query()
         .base("dc=261consulting,dc=com")
         .attributes("cn", "sn")
         .where("objectclass").is("person")
         .and("sn").is(lastName);

      return ldapTemplate.search(query,
         new AttributesMapper<String>() {
            public String mapFromAttributes(Attributes attrs)
               throws NamingException {

               return (String) attrs.get("cn").get();
            }
         });
   }
}
除了简化复杂搜索参数的构建之外,theLdapQueryBuilder及其相关类还提供了对搜索过滤器中任何不安全字符的正确转义。这可以防止“LDAP 注入”,用户可能会使用此类字符将不需要的操作注入到您的 LDAP 操作中。
LdapTemplate包括许多用于执行 LDAP 搜索的重载方法。这是为了适应尽可能多的不同用例和编程风格偏好。对于绝大多数用例,将LdapQuery输入作为输入的方法是推荐使用的方法。
AttributesMapper是处理搜索和查找数据时可以使用的唯一可用回调接口之一。有关替代方案 ,请参阅简化属性访问和操作。DirContextAdapter

有关 LDAP 的更多信息LdapQueryBuilder,请参阅高级 LDAP 查询

3.3. 动态构建专有名称

可分辨名称 ( ) 的标准 Java 实现LdapName在解析可分辨名称时表现良好。但是,在实际使用中,这种实现方式有很多缺点:

  • 实现是可变的LdapName,非常适合表示身份的对象。

  • 尽管具有可变性质,但用于通过 using 动态构建或修改专有名称的 APILdapName很麻烦。提取索引或(特别是)命名组件的值也有点尴尬。

  • 许多关于LdapName抛出检查异常的操作,需要try-catch针对错误通常是致命的并且无法以有意义的方式修复的情况的语句。

为了简化使用专有名称,Spring LDAP 提供了LdapNameBuilder一个.LdapUtilsLdapName

3.3.1。例子

本节介绍了前面几节中涵盖的主题的几个示例。第一个示例LdapName通过使用动态构建LdapNameBuilder

示例 5.LdapName通过使用动态构建一个LdapNameBuilder
package com.example.repo;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;

public class PersonRepoImpl implements PersonRepo {
  public static final String BASE_DN = "dc=example,dc=com";

  protected Name buildDn(Person p) {
    return LdapNameBuilder.newInstance(BASE_DN)
      .add("c", p.getCountry())
      .add("ou", p.getCompany())
      .add("cn", p.getFullname())
      .build();
  }
  ...
}

假设 aPerson具有以下属性:

属性名称 属性值

country

瑞典

company

某公司

fullname

一些人

然后,前面的代码将产生以下可分辨名称:

cn=Some Person, ou=Some Company, c=Sweden, dc=example, dc=com

以下示例使用以下示例从专有名称中提取值LdapUtils

示例 6. 通过使用从专有名称中提取值LdapUtils
package com.example.repo;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
public class PersonRepoImpl implements PersonRepo {
...
  protected Person buildPerson(Name dn, Attributes attrs) {
    Person person = new Person();
    person.setCountry(LdapUtils.getStringValue(dn, "c"));
    person.setCompany(LdapUtils.getStringValue(dn, "ou"));
    person.setFullname(LdapUtils.getStringValue(dn, "cn"));
    // Populate rest of person object using attributes.

    return person;
  }
}

由于 1.4 之前(包括 1.4)的 Java 版本根本没有提供任何公共专有名称实现,因此 Spring LDAP 1.x 提供了自己的实现,DistinguishedName. 这个实现本身有几个缺点,在 2.0 版中已被弃用。您现在应该LdapName与前面描述的实用程序一起使用。

3.4. 绑定和解除绑定

本节介绍如何添加和删除数据。下一节将介绍更新。

3.4.1。添加数据

在 Java LDAP 中插入数据称为绑定。这有点令人困惑,因为在 LDAP 术语中,“绑定”意味着完全不同的东西。JNDI 绑定执行 LDAP 添加操作,将具有指定专有名称的新条目与一组属性相关联。以下示例使用 添加数据LdapTemplate

示例 7. 使用属性添加数据
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   public void create(Person p) {
      Name dn = buildDn(p);
      ldapTemplate.bind(dn, null, buildAttributes(p));
   }

   private Attributes buildAttributes(Person p) {
      Attributes attrs = new BasicAttributes();
      BasicAttribute ocattr = new BasicAttribute("objectclass");
      ocattr.add("top");
      ocattr.add("person");
      attrs.put(ocattr);
      attrs.put("cn", "Some Person");
      attrs.put("sn", "Person");
      return attrs;
   }
}

手动属性构建——虽然枯燥而冗长——足以满足许多目的。但是,您可以进一步简化绑定操作,如使用简化属性访问和操作中DirContextAdapter所述。

3.4.2. 删除数据

在 Java LDAP 中删除数据称为解除绑定。JNDI 取消绑定执行 LDAP 删除操作,从 LDAP 树中删除与指定专有名称关联的条目。以下示例使用 删除数据LdapTemplate

示例 8. 删除数据
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   public void delete(Person p) {
      Name dn = buildDn(p);
      ldapTemplate.unbind(dn);
   }
}

3.5. 更新

在 Java LDAP 中,可以通过两种方式修改数据:使用rebind或使用modifyAttributes.

3.5.1。使用重新绑定进行更新

Arebind是修改数据的粗略方式。它基本上是一个unbind后跟一个bind。以下示例使用rebind

示例 9. 使用重新绑定进行修改
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   public void update(Person p) {
      Name dn = buildDn(p);
      ldapTemplate.rebind(dn, null, buildAttributes(p));
   }
}

3.5.2. 使用更新modifyAttributes

修改数据的一种更复杂的方法是使用modifyAttributes. 此操作采用一组显式属性修改并在特定条目上执行它们,如下所示:

示例 10. 使用 modifyAttributes 进行修改
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;
   ...
   public void updateDescription(Person p) {
      Name dn = buildDn(p);
      Attribute attr = new BasicAttribute("description", p.getDescription())
      ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
      ldapTemplate.modifyAttributes(dn, new ModificationItem[] {item});
   }
}

构建AttributesModificationItem数组是很多工作。然而,正如我们在Simplifying Attribute Access and Manipulation withDirContextAdapter中所描述的,Spring LDAP 为简化这些操作提供了更多帮助。

4. 简化属性访问和操作DirContextAdapter

Java LDAP API 的一个鲜为人知且可能被低估的特性是能够注册一个DirObjectFactory从找到的 LDAP 条目自动创建对象的能力。Spring LDAP 利用此功能DirContextAdapter在某些搜索和查找操作中返回实例。

DirContextAdapter是处理 LDAP 属性的有用工具,尤其是在添加或修改数据时。

4.1。搜索和查找使用ContextMapper

每当在 LDAP 树中找到一个条目时,Spring LDAP 都会使用它的属性和专有名称 (DN) 来构造一个DirContextAdapter. 这让我们可以使用 aContextMapper而不是 anAttributesMapper 来转换找到的值,如下所示:

示例 11. 使用 ContextMapper 进行搜索
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   ...
   private static class PersonContextMapper implements ContextMapper {
      public Object mapFromContext(Object ctx) {
         DirContextAdapter context = (DirContextAdapter)ctx;
         Person p = new Person();
         p.setFullName(context.getStringAttribute("cn"));
         p.setLastName(context.getStringAttribute("sn"));
         p.setDescription(context.getStringAttribute("description"));
         return p;
      }
   }

   public Person findByPrimaryKey(
      String name, String company, String country) {
      Name dn = buildDn(name, company, country);
      return ldapTemplate.lookup(dn, new PersonContextMapper());
   }
}

如上例所示,我们可以直接按名称检索属性值,而无需经过AttributesandAttribute类。这在使用多值属性时特别有用。从多值属性中提取值通常需要遍历NamingEnumeration从实现返回的属性值AttributesDirContextAdaptergetStringAttributes() orgetObjectAttributes()方法中为您执行此操作。以下示例使用该getStringAttributes方法:

示例 12. 使用获取多值属性值getStringAttributes()
private static class PersonContextMapper implements ContextMapper {
   public Object mapFromContext(Object ctx) {
      DirContextAdapter context = (DirContextAdapter)ctx;
      Person p = new Person();
      p.setFullName(context.getStringAttribute("cn"));
      p.setLastName(context.getStringAttribute("sn"));
      p.setDescription(context.getStringAttribute("description"));
      // The roleNames property of Person is an String array
      p.setRoleNames(context.getStringAttributes("roleNames"));
      return p;
   }
}

4.1.1。使用AbstractContextMapper

Spring LDAP 提供了一个抽象的基本实现ContextMapper,称为AbstractContextMapper. 此实现自动将提供的Object参数转换为DirContexOperations. 使用AbstractContextMapperPersonContextMapper前面所示的可以重写如下:

示例 13. 使用AbstractContextMapper
private static class PersonContextMapper extends AbstractContextMapper {
  public Object doMapFromContext(DirContextOperations ctx) {
     Person p = new Person();
     p.setFullName(ctx.getStringAttribute("cn"));
     p.setLastName(ctx.getStringAttribute("sn"));
     p.setDescription(ctx.getStringAttribute("description"));
     return p;
  }
}

4.2. 通过使用添加和更新数据DirContextAdapter

` 虽然在提取属性值时很有用,但DirContextAdapter对于管理添加和更新数据所涉及的细节更加强大。

4.2.1。通过使用添加数据DirContextAdapter

以下示例用于DirContextAdapter实现添加数据create中介绍的存储库方法的改进实现:

示例 14. 使用绑定DirContextAdapter
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   ...
   public void create(Person p) {
      Name dn = buildDn(p);
      DirContextAdapter context = new DirContextAdapter(dn);

      context.setAttributeValues("objectclass", new String[] {"top", "person"});
      context.setAttributeValue("cn", p.getFullname());
      context.setAttributeValue("sn", p.getLastname());
      context.setAttributeValue("description", p.getDescription());

      ldapTemplate.bind(context);
   }
}

请注意,我们使用DirContextAdapter实例作为绑定的第二个参数,它应该是一个Context. 第三个参数是null,因为我们没有明确指定属性。

还要注意设置属性值setAttributeValues()时使用的方法。objectclassobjectclass属性是多值的。与提取多值属性数据的麻烦类似,构建多值属性是一项繁琐而冗长的工作。通过使用该setAttributeValues()方法,您可以DirContextAdapter为您处理该工作。

4.2.2. 通过使用更新数据DirContextAdapter

我们之前看到使用更新modifyAttributes是推荐的方法,但是这样做需要我们执行计算属性修改和相应地构造ModificationItem数组的任务。 DirContextAdapter可以为我们做这一切,如下:

示例 15. 使用更新DirContextAdapter
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   ...
   public void update(Person p) {
      Name dn = buildDn(p);
      DirContextOperations context = ldapTemplate.lookupContext(dn);

      context.setAttributeValue("cn", p.getFullname());
      context.setAttributeValue("sn", p.getLastname());
      context.setAttributeValue("description", p.getDescription());

      ldapTemplate.modifyAttributes(context);
   }
}

当没有映射器传递给 aldapTemplate.lookup()时,结果是一个DirContextAdapter实例。当lookup方法返回 anObject时,lookupContext便捷方法方法自动将返回值转换为 a DirContextOperationsDirContextAdapter实现的接口)。

请注意,我们在createandupdate方法中有重复的代码。此代码从域对象映射到上下文。可以提取到单独的方法中,如下:

示例 16. 使用 DirContextAdapter 添加和修改
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;

   ...
   public void create(Person p) {
      Name dn = buildDn(p);
      DirContextAdapter context = new DirContextAdapter(dn);

      context.setAttributeValues("objectclass", new String[] {"top", "person"});
      mapToContext(p, context);
      ldapTemplate.bind(context);
   }

   public void update(Person p) {
      Name dn = buildDn(p);
      DirContextOperations context = ldapTemplate.lookupContext(dn);
      mapToContext(person, context);
      ldapTemplate.modifyAttributes(context);
   }

   protected void mapToContext (Person p, DirContextOperations context) {
      context.setAttributeValue("cn", p.getFullName());
      context.setAttributeValue("sn", p.getLastName());
      context.setAttributeValue("description", p.getDescription());
   }
}

4.3. DirContextAdapter和专有名称作为属性值

在 LDAP 中管理安全组时,通常具有表示专有名称的属性值。由于专有名称相等与字符串相等不同(例如,在专有名称相等中忽略空格和大小写差异),因此使用字符串相等计算属性修改不会按预期工作。

例如,如果一个member属性的值为cn=John Doe,ou=People并且我们调用ctx.addAttributeValue("member", "CN=John Doe, OU=People"),则该属性现在被认为具有两个值,即使字符串实际上表示相同的专有名称。

从 Spring LDAP 2.0 开始,为javax.naming.Name属性修改方法提供实例会DirContextAdapter 在计算属性修改时使用专有名称相等性。如果我们将前面的示例修改为 ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People")),它不会呈现修改,如下例所示:

示例 17. 使用 DirContextAdapter 修改组成员资格
public class GroupRepo implements BaseLdapNameAware {
    private LdapTemplate ldapTemplate;
    private LdapName baseLdapPath;

    public void setLdapTemplate(LdapTemplate ldapTemplate) {
        this.ldapTemplate = ldapTemplate;
    }

    public void setBaseLdapPath(LdapName baseLdapPath) {
        this.setBaseLdapPath(baseLdapPath);
    }

    public void addMemberToGroup(String groupName, Person p) {
        Name groupDn = buildGroupDn(groupName);
        Name userDn = buildPersonDn(
            person.getFullname(),
            person.getCompany(),
            person.getCountry());

        DirContextOperation ctx = ldapTemplate.lookupContext(groupDn);
        ctx.addAttributeValue("member", userDn);

        ldapTemplate.update(ctx);
    }

    public void removeMemberFromGroup(String groupName, Person p) {
        Name groupDn = buildGroupDn(String groupName);
        Name userDn = buildPersonDn(
            person.getFullname(),
            person.getCompany(),
            person.getCountry());

        DirContextOperation ctx = ldapTemplate.lookupContext(groupDn);
        ctx.removeAttributeValue("member", userDn);

        ldapTemplate.update(ctx);
    }

    private Name buildGroupDn(String groupName) {
        return LdapNameBuilder.newInstance("ou=Groups")
            .add("cn", groupName).build();
    }

    private Name buildPersonDn(String fullname, String company, String country) {
        return LdapNameBuilder.newInstance(baseLdapPath)
            .add("c", country)
            .add("ou", company)
            .add("cn", fullname)
            .build();
   }
}

在前面的示例中,我们实现BaseLdapNameAware了获取基本 LDAP 路径,如获取对基本 LDAP 路径的引用中所述。这是必要的,因为作为成员属性值的可分辨名称必须始终是目录根的绝对名称。

4.4. 完整的PersonRepository课程

为了说明 Spring LDAP 和 的有用性DirContextAdapter,以下示例显示了PersonLDAP 的完整 Repository 实现:

package com.example.repo;
import java.util.List;

import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;

import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.WhitespaceWildcardsFilter;

import static org.springframework.ldap.query.LdapQueryBuilder.query;

public class PersonRepoImpl implements PersonRepo {
   private LdapTemplate ldapTemplate;

   public void setLdapTemplate(LdapTemplate ldapTemplate) {
      this.ldapTemplate = ldapTemplate;
   }

   public void create(Person person) {
      DirContextAdapter context = new DirContextAdapter(buildDn(person));
      mapToContext(person, context);
      ldapTemplate.bind(context);
   }

   public void update(Person person) {
      Name dn = buildDn(person);
      DirContextOperations context = ldapTemplate.lookupContext(dn);
      mapToContext(person, context);
      ldapTemplate.modifyAttributes(context);
   }

   public void delete(Person person) {
      ldapTemplate.unbind(buildDn(person));
   }

   public Person findByPrimaryKey(String name, String company, String country) {
      Name dn = buildDn(name, company, country);
      return ldapTemplate.lookup(dn, getContextMapper());
   }

   public List findByName(String name) {
      LdapQuery query = query()
         .where("objectclass").is("person")
         .and("cn").whitespaceWildcardsLike("name");

      return ldapTemplate.search(query, getContextMapper());
   }

   public List findAll() {
      EqualsFilter filter = new EqualsFilter("objectclass", "person");
      return ldapTemplate.search(LdapUtils.emptyPath(), filter.encode(), getContextMapper());
   }

   protected ContextMapper getContextMapper() {
      return new PersonContextMapper();
   }

   protected Name buildDn(Person person) {
      return buildDn(person.getFullname(), person.getCompany(), person.getCountry());
   }

   protected Name buildDn(String fullname, String company, String country) {
      return LdapNameBuilder.newInstance()
        .add("c", country)
        .add("ou", company)
        .add("cn", fullname)
        .build();
   }

   protected void mapToContext(Person person, DirContextOperations context) {
      context.setAttributeValues("objectclass", new String[] {"top", "person"});
      context.setAttributeValue("cn", person.getFullName());
      context.setAttributeValue("sn", person.getLastName());
      context.setAttributeValue("description", person.getDescription());
   }

   private static class PersonContextMapper extends AbstractContextMapper<Person> {
      public Person doMapFromContext(DirContextOperations context) {
         Person person = new Person();
         person.setFullName(context.getStringAttribute("cn"));
         person.setLastName(context.getStringAttribute("sn"));
         person.setDescription(context.getStringAttribute("description"));
         return person;
      }
   }
}
在某些情况下,对象的专有名称 (DN) 是通过使用对象的属性来构造的。在前面的示例中,PersonDN 中使用了国家、公司和全名,这意味着更新这些属性中的任何一个实际上rename()除了更新Attribute值之外还需要使用操作移动 LDAP 树中的条目。rename()由于这是高度特定于实现的,因此您需要通过禁止用户更改这些属性或在需要时在您的update()方法中执行操作来跟踪自己。请注意,通过使用Object-Directory Mapping (ODM),如果您适当地注释域类,该库可以自动为您处理此问题。

5. 对象目录映射 (ODM)

对象关系映射框架(例如 Hibernate 和 JPA)为开发人员提供了使用注释将关系数据库表映射到 Java 对象的能力。Spring LDAP 项目通过以下多种方法提供了关于 LDAP 目录的类似功能LdapOperations

  • <T> T findByDn(Name dn, Class<T> clazz)

  • <T> T findOne(LdapQuery query, Class<T> clazz)

  • <T> List<T> find(LdapQuery query, Class<T> clazz)

  • <T> List<T> findAll(Class<T> clazz)

  • <T> List<T> findAll(Name base, SearchControls searchControls, Class<T> clazz)

  • <T> List<T> findAll(Name base, Filter filter, SearchControls searchControls, Class<T> clazz)

  • void create(Object entry)

  • void update(Object entry)

  • void delete(Object entry)

5.1。注释

使用对象映射方法管理的实体类需要使用org.springframework.ldap.odm.annotations包中的注释进行注释。可用的注释是:

  • @Entry:类级别注释,指示objectClass实体映射到的定义。(必需的)

  • @Id:表示实体DN。声明该属性的字段必须是javax.naming.Name类的派生词。(必需的)

  • @Attribute:表示目录属性到对象类字段的映射。

  • @DnAttribute:表示DN属性到对象类字段的映射。

  • @Transient: 表示该字段不是持久的,应该被OdmManager.

和注释需要在托管类上声明 @Entry。用于指定实体映射到哪些对象类,以及(可选)由该类表示的 LDAP 条目的目录根。需要声明为其映射字段的所有对象类。请注意,在创建托管类的新条目时,仅使用已声明的对象类。@Id@Entry

为了将目录条目视为与托管实体的匹配,目录条目声明的所有对象类都必须由@Entry注释声明。例如,假设您的 LDAP 树中有具有以下对象类的条目:inetOrgPerson,organizationalPerson,person,top. 如果您只对更改person对象类中定义的属性感兴趣,您可以@Entry使用@Entry(objectClasses = { "person", "top" }). 但是,如果要管理定义在inetOrgPersonobjectclass 中的属性,则需要使用以下内容:@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }).

注释用于将@Id条目的可分辨名称映射到字段。该字段必须是 的实例javax.naming.Name

@Attribute注解用于将对象类字段映射到实体字段 。@Attribute需要声明字段映射到的对象类属性的名称,并且可以选择声明 LDAP 属性的语法 OID,以保证精确匹配。 @Attribute还提供类型声明,它允许您指示属性是被 LDAP JNDI 提供程序视为基于二进制还是基于字符串。

@DnAttribute注释用于将对象类字段映射到条目的专有名称中的组件和从组件映射出来。当从目录树中读取条目时,带有注释的字段@DnAttribute会自动填充来自专有名称的适当值。只有类型的字段String可以用 注释@DnAttribute。不支持其他类型。如果指定了一个类index中所有@DnAttribute注解的属性,也可以在创建和更新条目时自动计算DN。对于更新方案,如果作为可分辨名称一部分的属性已更改,这还会自动处理树中的移动条目。

注释指示该@Transient字段应被对象目录映射忽略,并且不映射到基础 LDAP 属性。请注意,如果 a@DnAttribute不绑定到Attribute. 也就是说,它只是专有名称的一部分,而不是由对象属性表示。它还必须用 注释@Transient

5.2. 执行

当所有组件都已正确配置和注释后,LdapTemplate可以使用如下的对象映射方法:

示例 18. 执行
@Entry(objectClasses = { "person", "top" }, base="ou=someOu")
public class Person {
   @Id
   private Name dn;

   @Attribute(name="cn")
   @DnAttribute(value="cn", index=1)
   private String fullName;

   // No @Attribute annotation means this will be bound to the LDAP attribute
   // with the same value
   private String description;

   @DnAttribute(value="ou", index=0)
   @Transient
   private String company;

   @Transient
   private String someUnmappedField;
   // ...more attributes below
}


public class OdmPersonRepo {
   @Autowired
   private LdapTemplate ldapTemplate;

   public Person create(Person person) {
      ldapTemplate.create(person);
      return person;
   }

   public Person findByUid(String uid) {
      return ldapTemplate.findOne(query().where("uid").is(uid), Person.class);
   }

   public void update(Person person) {
      ldapTemplate.update(person);
   }

   public void delete(Person person) {
      ldapTemplate.delete(person);
   }

   public List<Person> findAll() {
      return ldapTemplate.findAll(Person.class);
   }

   public List<Person> findByLastName(String lastName) {
      return ldapTemplate.find(query().where("sn").is(lastName), Person.class);
   }
}

5.3. ODM 和专有名称作为属性值

LDAP 中的安全组通常包含一个多值属性,其中每个值都是系统中用户的可分辨名称。处理这些类型的属性时所涉及的困难在DirContextAdapter和作为属性值的可分辨名称中进行了讨论。

ODM 还支持javax.naming.Name属性值,使组修改变得容易,如以下示例所示:

示例 19. 示例组表示
@Entry(objectClasses = {"top", "groupOfUniqueNames"}, base = "cn=groups")
public class Group {

    @Id
    private Name dn;

    @Attribute(name="cn")
    @DnAttribute("cn")
    private String name;

    @Attribute(name="uniqueMember")
    private Set<Name> members;

    public Name getDn() {
        return dn;
    }

    public void setDn(Name dn) {
        this.dn = dn;
    }

    public Set<Name> getMembers() {
        return members;
    }

    public void setMembers(Set<Name> members) {
        this.members = members;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void addMember(Name member) {
        members.add(member);
    }

    public void removeMember(Name member) {
        members.remove(member);
    }
}

当您使用setMembers, addMemberremoveMember然后调用来修改组成员时ldapTemplate.update(),属性修改是通过使用专有名称相等来计算的,这意味着在确定它们是否相等时将忽略专有名称的文本格式。

6. 高级 LDAP 查询

本节介绍如何将 LDAP 查询与 Spring LDAP 一起使用。

6.1。LDAP 查询生成器参数

及其关联的LdapQueryBuilder类旨在支持可提供给 LDAP 搜索的所有参数。支持以下参数:

  • base:指定 LDAP 树中应该开始搜索的根 DN。

  • searchScope:指定搜索应遍历的 LDAP 树的深度。

  • attributes:指定要从搜索中返回的属性。默认为全部。

  • countLimit:指定从搜索返回的最大条目数。

  • timeLimit:指定搜索可能花费的最长时间。

  • 搜索过滤器:我们要查找的条目必须满足的条件。

AnLdapQueryBuilder是通过调用 的query方法创建的LdapQueryBuilder。它旨在作为一个流畅的构建器 API,其中首先定义基本参数,然后是过滤器规范调用。where一旦通过调用 的方法开始定义过滤条件LdapQueryBuilder,稍后的调用尝试(例如)base将被拒绝。基本搜索参数是可选的,但至少需要一个过滤器规范调用。以下查询搜索对象类为 的所有条目Person

示例 20. 搜索所有具有对象类的条目Person
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...

List<Person> persons = ldapTemplate.search(
      query().where("objectclass").is("person"),
      new PersonAttributesMapper());

以下查询搜索对象类为personcn(通用名称)为 的所有条目John Doe

示例 21. 搜索所有具有对象类的条目personcn=John Doe
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...

List<Person> persons = ldapTemplate.search(
      query().where("objectclass").is("person")
             .and("cn").is("John Doe"),
      new PersonAttributesMapper());

以下查询搜索对象类为person并从dc(域组件)开始的所有条目dc=261consulting,dc=com

示例 22. 搜索对象类person从以下位置开始的所有条目dc=261consulting,dc=com
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...

List<Person> persons = ldapTemplate.search(
      query().base("dc=261consulting,dc=com")
             .where("objectclass").is("person"),
      new PersonAttributesMapper());

以下查询返回cn对象类为person且从dc(域组件)为 的所有条目的(通用名称)属性dc=261consulting,dc=com

示例 23. 搜索所有类Person以 开头的条目dc=261consulting,dc=com,仅返回cn属性
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...

List<Person> persons = ldapTemplate.search(
      query().base("dc=261consulting,dc=com")
             .attributes("cn")
             .where("objectclass").is("person"),
      new PersonAttributesMapper());

以下查询用于搜索通用名称 ( )or的多个拼写:cn

示例 24. 使用or条件搜索
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
      query().where("objectclass").is("person"),
             .and(query().where("cn").is("Doe").or("cn").is("Doo"));
      new PersonAttributesMapper());

6.2. 筛选条件

前面的示例演示了 LDAP 过滤器中的简单等于条件。LDAP 查询构建器支持以下条件类型:

  • is: 指定一个等于 (=) 条件。

  • gte: 指定大于或等于 (>=) 条件。

  • lte:指定小于或等于 (⇐) 条件。

  • like: 指定一个“like”条件,其中通配符可以包含在查询中——例如,where("cn").like("J*hn Doe")导致以下过滤器:(cn=J*hn Doe).

  • whitespaceWildcardsLike: 指定所有空格都被通配符替换的条件——例如,where("cn").whitespaceWildcardsLike("John Doe")导致以下过滤器:.(cn=John*Doe)

  • isPresent: 指定检查属性是否存在的条件 - 例如,where("cn").isPresent()导致以下过滤器:(cn=*).

  • not:指定应否定当前条件 - 例如,where("sn").not().is("Doe)导致以下过滤器:(!(sn=Doe))

6.3. 硬编码过滤器

有时您可能希望将硬编码过滤器指定为LdapQuery. LdapQueryBuilder为此目的有两种方法:

  • filter(String hardcodedFilter): 使用指定的字符串作为过滤器。请注意,指定的输入字符串不会以任何方式被触及,这意味着如果您从用户输入构建过滤器,则此方法不是特别适合。

  • filter(String filterFormat, String…​ params): 使用指定的字符串作为 的输入MessageFormat,正确编码参数并将它们插入到过滤器字符串的指定位置。

  • filter(Filter filter):使用指定的过滤器。

您不能将硬编码过滤器方法与where前面描述的方法混合使用。它是一个或另一个。如果您使用 指定过滤器filter(),如果您稍后尝试调用,则会出现异常where

7.配置

配置 Spring LDAP 的推荐方法是使用自定义 XML 配置命名空间。要使其可用,您需要在 bean 文件中包含 Spring LDAP 命名空间声明,如下所示:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:ldap="http://www.springframework.org/schema/ldap"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/ldap https://www.springframework.org/schema/ldap/spring-ldap.xsd">

7.1。ContextSource配置

ContextSource是通过使用<ldap:context-source>标签来定义的。最简单的context-source声明要求您指定服务器 URL、用户名和密码,如下所示:

示例 25. 最简单的上下文源声明
<ldap:context-source
    username="cn=Administrator"
    password="secret"
    url="ldap://localhost:389" />

前面的示例LdapContextSource使用默认值(请参阅本段后面的表格)以及指定的 URL 和身份验证凭据创建一个。context-source 上的可配置属性如下(必填属性标有*):

表 1. ContextSource 配置属性
属性 默认 描述

id

contextSource

创建的 bean 的 ID。

username

使用 LDAP 服务器进行身份验证时使用的用户名(主体)。这通常是管理员用户的可分辨名称(例如,cn=Administrator),但可能因服务器和身份验证方法而异。authentication-source-ref如果未明确配置,则为必需。

password

使用 LDAP 服务器进行身份验证时使用的密码(凭据)。authentication-source-ref如果未明确配置,则为必需。

url*

要使用的 LDAP 服务器的 URL。URL 应采用以下格式:ldap://myserver.example.com:389. 对于 SSL 访问,请使用ldaps协议和适当的端口,例如ldaps://myserver.example.com:636. 如果您需要故障转移功能,您可以指定多个 URL,以逗号 ( ,) 分隔。

base

LdapUtils.emptyLdapName()

基本 DN。配置此属性后,向 LDAP 操作提供和从 LDAP 操作接收的所有专有名称都与指定的 LDAP 路径相关。这可以显着简化针对 LDAP 树的工作。但是,有时您需要访问基本路径。有关这方面的更多信息,请参阅获取对基本 LDAP 路径的引用

anonymous-read-only

false

定义是否使用匿名(未经身份验证)上下文执行只读操作。请注意,true不支持将此参数设置为与补偿事务支持一起设置并被拒绝。

referral

null

定义用于处理推荐的策略,如此处所述。有效值为:

  • ignore

  • follow

  • throw

native-pooling

false

指定是否应使用本机 Java LDAP 连接池。考虑改用 Spring LDAP 连接池。有关更多信息,请参阅池支持

authentication-source-ref

一个SimpleAuthenticationSource实例。

AuthenticationSource要使用的实例的ID (请参阅自定义主体和凭证管理)。

authentication-strategy-ref

一个SimpleDirContextAuthenticationStrategy实例。

DirContextAuthenticationStrategy要使用的实例的ID (请参阅自定义DirContext身份验证处理)。

base-env-props-ref

对自定义环境属性的引用,该Map属性应随环境一起提供给DirContexton 构造。

7.1.1. DirContext验证

DirContext创建实例用于在 LDAP 服务器上执行操作时,通常需要对这些上下文进行身份验证。Spring LDAP 提供了各种配置选项。

本节涉及在 的核心功能中对上下文进行身份验证ContextSource,以构造DirContextLdapTemplate. LDAP 通常仅用于用户身份验证,ContextSource也可以用于此目的。该过程在使用 Spring LDAP 的用户身份验证中进行了讨论。

默认情况下,为只读和读写操作创建经过身份验证的上下文。您应该指定要用于对元素进行身份验证的 LDAP 用户的username和。passwordcontext-source

如果是 LDAP 用户的专有名称 (DN),则无论是否在元素 上指定了 LDAP 路径username,它都必须是从 LDAP 树根开始的用户的完整 DN 。basecontext-source

一些 LDAP 服务器设置允许匿名只读访问。如果要使用匿名上下文进行只读操作,请将anonymous-read-only属性设置为true.

自定义DirContext认证处理

Spring LDAP 中使用的默认身份验证机制是SIMPLE身份验证。这意味着主体(由username属性指定)和凭据(由 指定password)在Hashtable发送到DirContext实现构造函数的 中设置。

很多时候这种处理是不够的。例如,LDAP 服务器通常设置为仅接受安全 TLS 通道上的通信。可能需要使用特定的 LDAP 代理身份验证机制或其他问题。

您可以通过提供对元素的DirContextAuthenticationStrategy实现引用来指定替代身份验证机制。context-source为此,请设置authentication-strategy-ref属性。

TLS

Spring LDAP 为需要 TLS 安全通道通信的 LDAP 服务器提供了两种不同的配置选项:DefaultTlsDirContextAuthenticationStrategyExternalTlsDirContextAuthenticationStrategy. 两种实现都在目标连接上协商 TLS 通道,但它们的实际身份验证机制不同。在DefaultTlsDirContextAuthenticationStrategy安全通道上应用 SIMPLE 身份验证(通过使用指定的usernameand password),ExternalTlsDirContextAuthenticationStrategy使用 EXTERNAL SASL 身份验证,应用通过使用系统属性配置的客户端证书进行身份验证。

由于不同的 LDAP 服务器实现对 TLS 通道的显式关闭响应不同(一些服务器要求连接正常关闭,而另一些不支持),TLSDirContextAuthenticationStrategy实现支持使用shutdownTlsGracefully参数指定关闭行为。如果此属性设置为false(默认值),则不会发生显式 TLS 关闭。如果是true,Spring LDAP 在关闭目标上下文之前尝试优雅地关闭 TLS 通道。

使用 TLS 连接时,您需要确保native-pooling关闭本机 LDAP 池功能(通过使用属性指定)。shutdownTlsGracefully如果设置为 ,这一点尤其重要false。但是,由于 TLS 通道协商过程非常昂贵,因此您可以通过使用 Spring LDAP 池支持来获得巨大的性能优势,如池支持中所述。
自定义主体和凭证管理

虽然用于创建已验证身份的用户名(即用户 DN)和密码Context是默认静态定义的(context-source元素配置中定义的在整个生命周期中使用ContextSource),但在某些情况下,这不是期望的行为。一种常见的情况是,在为该用户执行 LDAP 操作时,应使用当前用户的主体和凭据。您可以通过使用元素提供对元素AuthenticationSource实现的引用来修改默认行为,而不是显式指定and 。每次要创建经过身份验证的对象时,都会由主体和凭据查询。context-sourceauthentication-source-refusernamepasswordAuthenticationSourceContextSourceContext

如果您使用Spring Security ,您可以通过配置Spring Security 附带ContextSource的实例来确保始终使用当前登录用户的主体和凭据。SpringSecurityAuthenticationSource以下示例显示了如何执行此操作:

示例 26. 使用 SpringSecurityAuthenticationSource
<beans>
...
    <ldap:context-source
        url="ldap://localhost:389"
        authentication-source-ref="springSecurityAuthenticationSource"/>

    <bean id="springSecurityAuthenticationSource"
        class="org.springframework.security.ldap.authentication.SpringSecurityAuthenticationSource" />
...
</beans>
我们没有指定任何usernamepassword为。我们context-source在使用AuthenticationSource. 只有在使用默认行为时才需要这些属性。
使用时SpringSecurityAuthenticationSource,您需要使用 Spring SecurityLdapAuthenticationProvider来针对 LDAP 对用户进行身份验证。

7.1.2. 本机 Java LDAP 池

内部 Java LDAP 提供程序提供了一些非常基本的池功能。pooled您可以使用on 标志打开或关闭此 LDAP 连接池AbstractContextSource。默认值为false(自 1.3 版起)——即关闭本机 Java LDAP 池。LDAP 连接池的配置是通过使用System属性来管理的,因此您需要在 Spring Context 配置之外手动处理。您可以在此处找到本机池配置的详细信息。

内置的 LDAP 连接池有几个严重的缺陷,这就是 Spring LDAP 提供更复杂的 LDAP 连接池方法的原因,在Pooling Support中进行了描述。如果您需要池功能,这是推荐的方法。
无论池配置如何,该ContextSource#getContext(String principal, String credentials)方法始终明确不使用本机 Java LDAP 池,以便重置密码尽快生效。

7.1.3. 高级ContextSource配置

本节介绍了配置ContextSource.

自定义DirContext环境属性

在某些情况下,除了直接在context-source. 您应该在 a 中设置此类属性Map并在属性中引用它们base-env-props-ref

7.2. LdapTemplate配置

LdapTemplate是通过使用<ldap:ldap-template>元素来定义的。最简单的ldap-template声明是元素本身:

示例 27. 最简单的 ldap-template 声明
<ldap:ldap-template />

元素本身创建一个LdapTemplate具有默认 ID 的实例,引用 default ContextSource,它的 ID 应为(元素contextSource的默认值)。context-source

下表描述了 上的可配置属性ldap-template

表 2. LdapTemplate 配置属性
属性 默认 描述

id

ldapTemplate

创建的 bean 的 ID。

context-source-ref

contextSource

ContextSource要使用的实例的 ID 。

count-limit

0

搜索的默认计数限制。0 表示没有限制。

time-limit

0

搜索的默认时间限制,以毫秒为单位。0 表示没有限制。

search-scope

SUBTREE

搜索的默认搜索范围。有效值为:

  • OBJECT

  • ONELEVEL

  • SUBTREE

ignore-name-not-found

false

指定是否NameNotFoundException应在搜索中忽略 a。设置此属性以true使由无效搜索库引起的错误被无声地吞噬。

ignore-partial-result

false

指定是否PartialResultException应在搜索中忽略。某些 LDAP 服务器在引用方面存在问题。这些通常应该自动遵循。但是,如果这不起作用,它会以PartialResultException. 将此属性设置为true可以解决此问题。

odm-ref

ObjectDirectoryMapper要使用的实例的 ID 。默认是默认配置的DefaultObjectDirectoryMapper

7.3. 获取对基本 LDAP 路径的引用

如前所述,您可以为 提供基本 LDAP 路径ContextSource,指定 LDAP 树中所有操作都与之相关的根。这意味着您在整个系统中只使用相对可分辨的名称,这通常非常方便。但是,在某些情况下,您可能需要访问基本路径才能构建相对于 LDAP 树的实际根的完整 DN。一个例子是使用 LDAP 组(例如,groupOfNames对象类)。在这种情况下,每个组成员属性值都需要是被引用成员的完整 DN。

出于这个原因,Spring LDAP 具有一种机制,通过该机制,任何 Spring 控制的 bean 都可以在启动时提供基本路径。对于要通知基本路径的 bean,需要做好两件事。首先,想要基本路径引用的 bean 需要实现BaseLdapNameAware接口。其次,您需要BaseLdapPathBeanPostProcessor在应用程序上下文中定义一个。下面的例子展示了如何实现BaseLdapNameAware

示例 28. 实现BaseLdapNameAware
package com.example.service;
public class PersonService implements PersonService, BaseLdapNameAware {
   ...
   private LdapName basePath;

   public void setBaseLdapPath(LdapName basePath) {
      this.basePath = basePath;
   }
   ...
   private LdapName getFullPersonDn(Person person) {
      return LdapNameBuilder.newInstance(basePath)
          .add(person.getDn())
          .build();
   }
   ...
}

以下示例显示了如何定义 a BaseLdapPathBeanPostProcessor

示例 29. 在 ApplicationContext 中指定 BaseLdapPathBeanPostProcessor
<beans>
   ...
   <ldap:context-source
          username="cn=Administrator"
          password="secret"
          url="ldap://localhost:389"
          base="dc=261consulting,dc=com" />
   ...
   <bean class="org.springframework.ldap.core.support.BaseLdapPathBeanPostProcessor" />
</beans>

的默认行为是BaseLdapPathBeanPostProcessor使用. 如果定义了多个,则需要通过设置属性来指定使用哪一个。BaseLdapPathSourceAbstractContextSourceApplicationContextBaseLdapPathSourcebaseLdapPathSourceName

8. Spring LDAP 存储库

Spring LDAP 内置了对 Spring Data 存储库的支持。此处描述了基本功能和配置。使用 Spring LDAP 存储库时,您应该记住以下内容:

  • <ldap:repositories>您可以通过使用XML 配置中的元素或使用@EnableLdapRepositories配置类上的注释来启用 Spring LDAP 存储库。

  • 要在自动生成的存储库中包含对LdapQuery参数的支持,请让您的接口扩展LdapRepository而不是CrudRepository.

  • 所有 Spring LDAP 存储库都必须与使用 ODM 注释进行注释的实体一起使用,如Object-Directory Mapping (ODM)中所述。

  • 由于所有 ODM 托管类都必须有一个专有名称作为 ID,因此所有 Spring LDAP 存储库都必须将 ID 类型参数设置为javax.naming.Name. 内置LdapRepository只接受一个类型参数:托管实体类,默认 ID 为javax.naming.Name.

  • 由于 LDAP 协议的特殊性,Spring LDAP 存储库不支持分页和排序。

8.1。查询DSL支持

Spring LDAP 中包含基本的 QueryDSL 支持。这种支持包括以下内容:

  • 一个注释处理器,称为LdapAnnotationProcessor,用于基于 Spring LDAP ODM 注释生成 QueryDSL 类。有关 ODM 注释的更多信息,请参阅对象目录映射 (ODM)

  • 一个 Query 实现,称为QueryDslLdapQuery,用于在代码中构建和运行 QueryDSL 查询。

  • Spring Data 存储库支持 QueryDSL 谓词。QueryDslPredicateExecutor包括许多带有适当参数的附加方法。您可以扩展此接口并LdapRepository在您的存储库中包含此支持。

9. 汇集支持

合并 LDAP 连接有助于减轻为每个 LDAP 交互创建新 LDAP 连接的开销。虽然存在Java LDAP 池支持,但它的配置选项和功能受到限制,例如连接验证和池维护。Spring LDAP 为每个基础的详细池配置提供支持ContextSource

通过向应用程序上下文配置中的元素提供<ldap:pooling />子元素来提供池支持。<ldap:context-source />只读和读写DirContext对象分开池化(如果anonymous-read-only指定)。Jakarta Commons-Pool用于提供底层池实现。

9.1。DirContext验证

验证池连接是使用自定义池库与 JDK 提供的 LDAP 池功能相比的主要动机。验证允许DirContext检查池连接以确保在将它们从池中检出、将它们检入池中或在池中空闲时它们仍然正确连接和配置。

如果配置了连接验证,则使用DefaultDirContextValidator. DefaultDirContextValidator执行一个DirContext.search(String, String, SearchControls), 具有一个空名称、一个过滤器"objectclass=*", 并SearchControls设置为限制具有唯一objectclass属性和 500 毫秒超时的单个结果。如果返回NamingEnumeration有结果,则DirContext通过验证。如果没有返回结果或抛出异常,则DirContext验证失败。默认设置应在大多数 LDAP 服务器上无需更改配置即可使用,并提供验证DirContext. 如果您需要自定义,可以使用池配置中描述的验证配置属性来实现。

如果连接抛出被认为是非瞬态的异常,连接将自动失效。例如,如果一个DirContext实例抛出 a javax.naming.CommunicationException,它会被解释为一个非暂时性错误,并且该实例会自动失效,而无需额外testOnReturn操作的开销。被解释为非瞬态的异常是nonTransientExceptions使用PoolingContextSource.

9.2. 池配置

以下属性可<ldap:pooling />用于配置 DirContext 池的元素:

表 3. 池化配置属性
属性 默认 描述

max-active

8

可以同时从此池中分配的每种类型(只读或读写)的最大活动连接数。您可以无限制地使用非正数。

max-total

-1

可以同时从此池中分配的活动连接的最大总数(适用于所有类型)。您可以无限制地使用非正数。

max-idle

8

在不释放额外连接的情况下,可以在池中保持空闲状态的每种类型(只读或读写)的最大活动连接数。您可以无限制地使用非正数。

min-idle

0

可以在池中保持空闲而不创建额外连接的每种类型(只读或读写)的最小活动连接数。您可以使用零(默认值)来创建无。

max-wait

-1

池等待(当没有可用连接时)在抛出异常之前返回连接的最大毫秒数。您可以使用非正数无限期等待。

when-exhausted

BLOCK

指定池耗尽时的行为。

  • 当池耗尽时,该FAIL选项会抛出。NoSuchElementException

  • BLOCK选项会等到有新对象可用。如果是肯定的并且在时间到期max-wait后没有新对象可用,则抛出。max-waitNoSuchElementException

  • GROW选项创建并返回一个新对象(基本上使max-active无意义)。

test-on-borrow

false

对象在从池中借用之前是否经过验证。如果对象验证失败,则将其从池中删除,并尝试借用另一个对象。

test-on-return

false

对象在返回池之前是否经过验证。

test-while-idle

false

对象是否由空闲对象驱逐器验证(如果有)。如果一个对象验证失败,它就会从池中删除。

eviction-run-interval-millis

-1

空闲对象驱逐线程运行之间休眠的毫秒数。当非正数时,不运行空闲对象驱逐线程。

tests-per-eviction-run

3

每次运行空闲对象驱逐线程(如果有)期间要检查的对象数。

min-evictable-time-millis

1000 * 60 * 30(30分钟)

一个对象在被空闲对象驱逐者(如果有的话)驱逐之前可以在池中闲置的最短时间。

validation-query-base

LdapUtils.emptyName()

验证连接时要使用的搜索库。仅在指定test-on-borrowtest-on-return或时test-while-idle使用。

validation-query-filter

objectclass=*

验证连接时要使用的搜索过滤器。仅在指定test-on-borrowtest-on-return或时test-while-idle使用。

validation-query-search-controls-ref

null; 上面描述了默认搜索控制设置。

SearchControls验证连接时要使用的实例的 ID 。仅在指定test-on-borrowtest-on-return或时test-while-idle使用。

non-transient-exceptions

javax.naming.CommunicationException

逗号分隔的Exception类列表。就急切失效而言,列出的异常被认为是非瞬态的。如果任何列出的异常(或它们的子类)被对池DirContext实例的调用抛出,该对象将自动失效,无需任何额外的 testOnReturn 操作。

9.3. 池 2 配置

以下属性可用于配置池的<ldap:pooling2 />元素:DirContext

表 4. 池化配置属性
属性 默认 描述

max-total

-1

可以同时从此池中分配的活动连接的最大总数(适用于所有类型)。您可以无限制地使用非正数。

max-total-per-key

8

每个键的池分配的对象实例数的限制(签出或空闲)。当达到限制时,子池被耗尽。负值表示没有限制。

max-idle-per-key

8

可以在池中保持空闲状态而不会释放额外连接的每种类型(只读或读写)的最大活动连接数。负值表示没有限制。

min-idle-per-key

0

可以在池中保持空闲且不创建额外连接的每种类型(只读或读写)的最小活动连接数。您可以使用零(默认值)来创建无。

max-wait

-1

池等待(当没有可用连接时)在抛出异常之前返回连接的最大毫秒数。您可以使用非正数无限期等待。

block-when-exhausted

true

是否等到有新对象可用。如果 max-wait 为正,则如果在时间到期NoSuchElementException后没有新对象可用,则抛出a。maxWait

test-on-create

false

借用前是否验证对象。如果对象验证失败,则借用失败。

test-on-borrow

false

对象在从池中借用之前是否经过验证的指示符。如果对象验证失败,则将其从池中删除,并尝试借用另一个对象。

test-on-return

false

对象在返回池之前是否经过验证的指示符。

test-while-idle

false

对象是否由空闲对象驱逐器(如果有)验证的指示符。如果一个对象验证失败,它就会从池中删除。

eviction-run-interval-millis

-1

空闲对象驱逐线程运行之间休眠的毫秒数。当非正数时,不运行空闲对象驱逐线程。

tests-per-eviction-run

3

每次运行空闲对象驱逐线程(如果有)期间要检查的对象数。

min-evictable-time-millis

1000 * 60 * 30(30分钟)

一个对象在被空闲对象驱逐者(如果有的话)驱逐之前可以在池中闲置的最短时间。

soft-min-evictable-time-millis

-1

一个对象在它有资格被空闲对象驱逐者驱逐之前可以在池中闲置的最短时间,附加条件是每个键至少有最小数量的对象实例保留在池中。min-evictable-time-millis如果设置为正值,则会覆盖此设置。

eviction-policy-class

org.apache.commons.pool2.impl.DefaultEvictionPolicy

此池使用的逐出策略实现。池尝试使用线程上下文类加载器加载类。如果失败,池会尝试使用加载该类的类加载器来加载该类。

fairness

false

该池为等待公平借用连接的线程提供服务。true意味着等待线程的服务就像在 FIFO 队列中等待一样。

jmx-enable

true

池的平台 MBean 服务器启用了 JMX。

jmx-name-base

null

JMX 名称库,用作分配给启用 JMX 的池的名称的一部分。

jmx-name-prefix

pool

JMX 名称前缀,用作分配给启用 JMX 的池的名称的一部分。

lifo

true

池是否具有相对于空闲对象或作为 FIFO(先进先出)队列的 LIFO(后进先出)行为的指示器。LIFO 总是返回池中最近使用的对象,而 FIFO 总是返回空闲对象池中最旧的对象

validation-query-base

LdapUtils.emptyPath()

用于验证搜索的基本 DN。

validation-query-filter

objectclass=*

用于验证查询的过滤器。

validation-query-search-controls-ref

null; 上面描述了默认搜索控制设置。

SearchControls验证连接时要使用的实例的 ID 。仅在指定test-on-borrowtest-on-return或时使用test-while-idle

non-transient-exceptions

javax.naming.CommunicationException

逗号分隔的Exception类列表。就急切失效而言,列出的异常被认为是非瞬态的。如果任何列出的异常(或它们的子类)被对池DirContext实例的调用抛出,该对象将自动失效,无需任何额外的 testOnReturn 操作。

9.4。配置

配置池化需要添加一个<ldap:pooling>嵌套在元素中的<ldap:context-source>元素,如下:

<beans>
   ...
    <ldap:context-source
        password="secret" url="ldap://localhost:389" username="cn=Manager">
        <ldap:pooling />
    </ldap:context-source>
   ...
</beans>

在实际情况中,您可能会配置池选项并启用连接验证。前面的例子演示了一般的想法。

9.4.1。验证配置

以下示例在将每个对象DirContext传递给客户端应用程序之前对其进行测试,并测试DirContext池中一直处于空闲状态的对象:

<beans>
   ...
    <ldap:context-source
        username="cn=Manager" password="secret" url="ldap://localhost:389" >
        <ldap:pooling
            test-on-borrow="true"
            test-while-idle="true" />
    </ldap:context-source>
   ...
</beans>

9.5。已知的问题

本节描述人们使用 Spring LDAP 时有时会出现的问题。目前,它涵盖了以下问题:

9.5.1。自定义身份验证

PoolingContextSource假设DirContext从中检索到的所有对象都ContextSource.getReadOnlyContext()具有相同的环境,并且同样地,DirContext从其中检索到的所有对象ContextSource.getReadWriteContext()都具有相同的环境。这意味着用 a 包装LdapContextSource配置的AuthenticationSourceaPoolingContextSource不能按预期工作。将使用第一个用户的凭据填充池,并且除非需要新连接,否则不会AuthenticationSource为请求线程指定的用户填充后续上下文请求。

10. 添加缺失的重载 API 方法

本节介绍如何添加您自己的重载 API 方法来实现新功能。

10.1。实施自定义搜索方法

LdapTemplate包含DirContext. 但是,我们没有为每个方法签名提供替代方案,主要是因为它们太多了。但是,我们提供了一种方法来调用DirContext您想要的任何方法,并且仍然可以获得所LdapTemplate提供的好处。

假设您要调用以下DirContext方法:

NamingEnumeration search(Name name, String filterExpr, Object[] filterArgs, SearchControls ctls)

中没有对应的重载方法LdapTemplate。解决这个问题的方法是使用自定义SearchExecutor实现,如下:

public interface SearchExecutor {
   public NamingEnumeration executeSearch(DirContext ctx) throws NamingException;
}

在您的自定义执行程序中,您可以访问一个DirContext对象,您可以使用它来调用您想要的方法。然后,您可以提供一个负责映射属性和收集结果的处理程序。例如,您可以使用 的可用实现之一CollectingNameClassPairCallbackHandler,它将映射结果收集到内部列表中。为了实际执行搜索,您需要调用将执行程序和处理程序作为参数的search方法。LdapTemplate最后,您需要返回处理程序收集的任何内容。以下示例显示了如何执行所有这些操作:

示例 30. 使用SearchExecutor和的自定义搜索方法AttributesMapper
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   ...
   public List search(final Name base, final String filter, final String[] params,
         final SearchControls ctls) {
      SearchExecutor executor = new SearchExecutor() {
         public NamingEnumeration executeSearch(DirContext ctx) {
            return ctx.search(base, filter, params, ctls);
         }
      };

      CollectingNameClassPairCallbackHandler handler =
         new AttributesMapperCallbackHandler(new PersonAttributesMapper());

      ldapTemplate.search(executor, handler);
      return handler.getList();
   }
}

如果您更喜欢ContextMapperAttributesMapper以下示例显示了它的外观:

示例 31. 使用SearchExecutor和的自定义搜索方法ContextMapper
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   ...
   public List search(final Name base, final String filter, final String[] params,
         final SearchControls ctls) {
      SearchExecutor executor = new SearchExecutor() {
         public NamingEnumeration executeSearch(DirContext ctx) {
            return ctx.search(base, filter, params, ctls);
         }
      };

      CollectingNameClassPairCallbackHandler handler =
         new ContextMapperCallbackHandler(new PersonContextMapper());

      ldapTemplate.search(executor, handler);
      return handler.getList();
   }
}
当您使用 时ContextMapperCallbackHandler,您必须确保您已经调用setReturningObjFlag(true)了您的SearchControls实例。

10.2. 实现其他自定义上下文方法

与自定义search方法一样,您实际上可以DirContext使用 a调用任何方法ContextExecutor,如下所示:

public interface ContextExecutor {
   public Object executeWithContext(DirContext ctx) throws NamingException;
}

实现 customContextExecutor时,您可以选择使用executeReadOnly()executeReadWrite()方法。假设您要调用以下方法:

Object lookupLink(Name name)

该方法在 中可用DirContext,但在 中没有匹配的方法LdapTemplate。它是一种查找方法,因此它应该是只读的。我们可以按如下方式实现:

示例 32. 使用自定义DirContext方法ContextExecutor
package com.example.repo;

public class PersonRepoImpl implements PersonRepo {
   ...
   public Object lookupLink(final Name name) {
      ContextExecutor executor = new ContextExecutor() {
         public Object executeWithContext(DirContext ctx) {
            return ctx.lookupLink(name);
         }
      };

      return ldapTemplate.executeReadOnly(executor);
   }
}

executeReadWrite()同理,您可以使用该方法进行读写操作。

11. 处理DirContext

本节介绍如何处理DirContext,包括预处理和后处理。

11.1。自定义DirContext预处理和后处理

在某些情况下,您可能希望DirContext在搜索操作之前和之后执行操作。用于此的接口称为DirContextProcessor. 以下清单显示了该DirContextProcessor界面:

public interface DirContextProcessor {
   public void preProcess(DirContext ctx) throws NamingException;
   public void postProcess(DirContext ctx) throws NamingException;
}

该类LdapTemplate有一个 search 方法,它采用 a DirContextProcessor,如下所示:

public void search(SearchExecutor se, NameClassPairCallbackHandler handler,
   DirContextProcessor processor) throws DataAccessException;

在搜索操作之前,在给定实例preProcess上调用该方法。DirContextProcessor在搜索运行并NamingEnumeration处理结果后,调用该postProcess方法。这使您可以对DirContext要在搜索中使用的 执行操作,并检查DirContext执行搜索的时间。这可能非常有用(例如,在处理请求和响应控件时)。

当您不需要自定义时,您还可以使用以下便捷方法SearchExecutor

public void search(Name base, String filter,
   SearchControls controls, NameClassPairCallbackHandler handler, DirContextProcessor processor)

public void search(String base, String filter,
   SearchControls controls, NameClassPairCallbackHandler handler, DirContextProcessor processor)

public void search(Name base, String filter,
   SearchControls controls, AttributesMapper mapper, DirContextProcessor processor)

public void search(String base, String filter,
   SearchControls controls, AttributesMapper mapper, DirContextProcessor processor)

public void search(Name base, String filter,
   SearchControls controls, ContextMapper mapper, DirContextProcessor processor)

public void search(String base, String filter,
   SearchControls controls, ContextMapper mapper, DirContextProcessor processor)

11.2. 实现请求控制DirContextProcessor

LDAPv3 协议使用“控件”来发送和接收附加数据以影响预定义操作的行为。为了简化请求控制的实现DirContextProcessor,Spring LDAP 提供了AbstractRequestControlDirContextProcessor基类。此类处理从 中检索当前请求控件LdapContext,调用用于创建请求控件的模板方法,并将其添加到LdapContext. 您在子类中所要做的就是实现调用的模板方法createRequestControl以及postProcess执行搜索后需要执行的任何操作的方法。以下清单显示了相关签名:

public abstract class AbstractRequestControlDirContextProcessor implements
      DirContextProcessor {

   public void preProcess(DirContext ctx) throws NamingException {
      ...
   }

   public abstract Control createRequestControl();
}

一个典型DirContextProcessor的类似于下面的例子:

示例 33. 请求控制DirContextProcessor实现
package com.example.control;

public class MyCoolRequestControl extends AbstractRequestControlDirContextProcessor {
   private static final boolean CRITICAL_CONTROL = true;
   private MyCoolCookie cookie;
   ...
   public MyCoolCookie getCookie() {
      return cookie;
   }

   public Control createRequestControl() {
      return new SomeCoolControl(cookie.getCookie(), CRITICAL_CONTROL);
   }

   public void postProcess(DirContext ctx) throws NamingException {
      LdapContext ldapContext = (LdapContext) ctx;
      Control[] responseControls = ldapContext.getResponseControls();

      for (int i = 0; i < responseControls.length; i++) {
         if (responseControls[i] instanceof SomeCoolResponseControl) {
            SomeCoolResponseControl control = (SomeCoolResponseControl) responseControls[i];
            this.cookie = new MyCoolCookie(control.getCookie());
         }
      }
   }
}
确保LdapContextSource在使用控件时使用。该Control接口特定于 LDAPv3,需要LdapContext使用它而不是DirContext. 如果使用AbstractRequestControlDirContextProcessor不是 的参数调用子类LdapContext,则会抛出IllegalArgumentException

11.3. 分页搜索结果

某些搜索可能会返回大量结果。当没有简单的方法过滤掉较小的数量时,让服务器每次调用时只返回一定数量的结果是很方便的。这被称为“分页搜索结果”。然后可以显示结果的每个“页面”,并带有指向下一页和上一页的链接。如果没有此功能,客户端必须手动将搜索结果限制为页面或检索整个结果,然后将其切成合适大小的页面。前者会相当复杂,而后者会消耗不必要的内存。

一些 LDAP 服务器支持PagedResultsControl,它要求 LDAP 服务器以指定大小的页面返回搜索操作的结果。用户通过控制调用搜索的速率来控制返回页面的速率。但是,您必须在调用之间跟踪 cookie。服务器使用此 cookie 来跟踪上次使用分页结果请求调用它时的中断位置。

LdapContext如前几节所述,Spring LDAP 通过使用 的预处理和后处理的概念来提供对分页结果的支持。它通过使用PagedResultsDirContextProcessor类来做到这一点。该类PagedResultsDirContextProcessor创建PagedResultsControl具有请求的页面大小的 a 并将其添加到LdapContext. 搜索后,它获取PagedResultsResponseControl并检索分页结果 cookie,这是保持连续分页结果请求之间的上下文所必需的。

以下示例显示了如何使用分页搜索结果功能:

示例 34. 使用分页结果PagedResultsDirContextProcessor
public List<String> getAllPersonNames() {
  final SearchControls searchControls = new SearchControls();
  searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

  final PagedResultsDirContextProcessor processor =
        new PagedResultsDirContextProcessor(PAGE_SIZE);

  return SingleContextSource.doWithSingleContext(
        contextSource, new LdapOperationsCallback<List<String>>() {

      @Override
      public List<String> doWithLdapOperations(LdapOperations operations) {
        List<String> result = new LinkedList<String>();

        do {
          List<String> oneResult = operations.search(
            "ou=People",
            "(&(objectclass=person))",
            searchControls,
            CN_ATTRIBUTES_MAPPER,
            processor);
          result.addAll(oneResult);
        } while(processor.hasMore());

        return result;
      }
  });
}
要使分页结果 cookie 继续有效,您必须对每个分页结果调用使用相同的底层连接。您可以使用 , 来完成此操作SingleContextSource,如前面的示例所示。

12. 交易支持

习惯使用关系数据库工作的程序员来到 LDAP 世界时,常常对没有事务的概念表示惊讶。它没有在协议中指定,并且没有 LDAP 服务器支持它。认识到这可能是一个主要问题,Spring LDAP 提供对客户端的支持,补偿 LDAP 资源上的事务。

LDAP 事务支持由管理 Spring 对 LDAP 操作的事务支持ContextSourceTransactionManager的实现提供。PlatformTransactionManager与它的合作者一起,它跟踪事务中执行的 LDAP 操作,记录每个操作之前的状态,并在事务需要回滚时采取措施恢复初始状态。

除了实际的事务管理之外,Spring LDAP 事务支持还确保在DirContext整个事务中使用相同的实例。也就是说,DirContext直到事务完成才真正关闭,从而可以更有效地使用资源。

虽然 Spring LDAP 用于提供事务支持的方法对于许多情况来说已经足够了,但它绝不是传统意义上的“真实”事务。服务器完全不知道事务,因此(例如),如果连接断开,则无法回滚事务。虽然应该仔细考虑这一点,但还应该注意的是,另一种方法是在没有任何事务支持的情况下运行。Spring LDAP 的事务支持非常好。
除了原始操作所需的工作之外,客户端事务支持增加了一些开销。虽然这种开销在大多数情况下不必担心,但如果您的应用程序不在同一个事务中执行多个 LDAP 操作(例如,modifyAttributes后跟rebind),或者如果不需要与 JDBC 数据源的事务同步(请参阅JDBC Transaction Integration),您通过使用 LDAP 事务支持获得的收益很少。

12.1. 配置

如果您习惯于配置 Spring 事务,那么配置 Spring LDAP 事务应该看起来非常熟悉。你可以用 注释你的事务类@Transactional,创建一个TransactionManager实例,并<tx:annotation-driven>在你的 bean 配置中包含一个元素。以下示例显示了如何执行此操作:

<ldap:context-source
       url="ldap://localhost:389"
       base="dc=example,dc=com"
       username="cn=Manager"
       password="secret" />

<ldap:ldap-template id="ldapTemplate" />
<ldap:transaction-manager>
    <!--
    Note this default configuration will not work for more complex scenarios;
    see below for more information on RenamingStrategies.
    -->
   <ldap:default-renaming-strategy />
</ldap:transaction-manager>

<!--
   The MyDataAccessObject class is annotated with @Transactional.
-->
<bean id="myDataAccessObject" class="com.example.MyRepository">
  <property name="ldapTemplate" ref="ldapTemplate" />
</bean>

<tx:annotation-driven />
...
虽然此设置适用于大多数简单的用例,但一些更复杂的场景需要额外的配置。具体来说,如果您需要在事务中创建或删除子树,则需要使用替代方法TempEntryRenamingStrategy,如重命名策略中所述。

在实际情况中,您可能会在服务对象级别而不是存储库级别上应用事务。前面的例子演示了一般的想法。

12.2. JDBC 事务集成

使用 LDAP 时的一个常见用例是一些数据存储在 LDAP 树中,而其他数据存储在关系数据库中。在这种情况下,事务支持变得更加重要,因为不同资源的更新应该是同步的。

虽然不支持实际的 XA 事务,但通过向元素提供data-source-ref属性来支持在概念上将 JDBC 和 LDAP 访问包装在同一事务中。<ldap:transaction-manager>这将创建一个ContextSourceAndDataSourceTransactionManager,然后虚拟地管理这两个事务,就好像它们是一个事务一样。执行提交时,始终首先执行操作的 LDAP 部分,如果 LDAP 提交失败,则让两个事务回滚。事务的 JDBC 部分的管理方式与 中完全相同DataSourceTransactionManager,只是不支持嵌套事务。以下示例显示了ldap:transaction-manager具有data-source-ref属性的元素:

<ldap:transaction-manager data-source-ref="dataSource" >
  <ldap:default-renaming-strategy />
<ldap:transaction-manager />
提供的支持都是客户端的。包装的事务不是 XA 事务。不执行两阶段提交,因为 LDAP 服务器无法对其结果进行投票。

session-factory-ref您可以通过向元素提供属性来为 Hibernate 集成完成相同的操作<ldap:transaction-manager>,如下所示:

<ldap:transaction-manager session-factory-ref="dataSource" >
  <ldap:default-renaming-strategy />
<ldap:transaction-manager />

12.3. LDAP 补偿事务解释

bindSpring LDAP 通过在每次修改操作( 、unbindrebindmodifyAttributesrename)之前在 LDAP 树中记录状态来管理补偿事务。这让系统在事务需要回滚时执行补偿操作。

在许多情况下,补偿操作非常简单。例如,一个操作的补偿回滚操作bind是取消绑定条目。然而,由于 LDAP 数据库的某些特殊特性,其他操作需要不同的、更复杂的方法。具体来说,并非总是可以获得所有Attributes条目的值,使得上述策略不足以(例如)unbind操作。

这就是为什么在 Spring LDAP 托管事务中执行的每个修改操作在内部都分为四个不同的操作:记录操作、准备操作、提交操作和回滚操作。下表描述了每个 LDAP 操作:

LDAP 操作 记录 准备 犯罪 回滚

bind

记录要绑定的条目的DN。

绑定条目。

无操作。

使用记录的 DN 解绑该条目。

rename

记录原始和目标 DN。

重命名条目。

无操作。

将条目重命名回其原始 DN。

unbind

记录原始DN并计算临时DN。

将条目重命名为临时位置。

解绑临时条目。

将临时位置的条目重命名回其原始 DN。

rebind

记录原始DN和新DNAttributes并计算临时DN。

将条目重命名为临时位置。

在原始 DN 处绑定新Attributes条目,并从其临时位置解除原始条目的绑定。

将临时位置的条目重命名回其原始 DN。

modifyAttributes

记录要修改的条目的 DN,并计算ModificationItem要完成的修改的补偿实例。

执行modifyAttributes操作。

无操作。

modifyAttributes使用计算出的补偿ModificationItem实例执行操作。

Javadoc中提供了有关 Spring LDAP 事务支持的内部工作的更详细描述。

12.3.1. 重命名策略

如上节表格所述,某些操作的事务管理需要将受操作影响的原始条目暂时重命名,然后才能在提交中进行实际修改。计算条目的临时 DN 的方式由配置中声明TempEntryRenamingStrategy的子元素中指定的 a管理。<ldap:transaction-manager >Spring LDAP 包括两个实现:

  • DefaultTempEntryRenamingStrategy(默认):使用<ldap:default-renaming-strategy />元素指定。将后缀添加到条目 DN 的最不重要部分。例如,对于 DN cn=john doe, ou=users,此策略返回 DN 的临时 DN cn=john doe_temp, ou=users。您可以通过设置temp-suffix属性来配置后缀。

  • DifferentSubtreeTempEntryRenamingStrategy: 使用<ldap:different-subtree-renaming-strategy />元素指定。它将子树 DN 附加到 DN 的最不重要部分。这样做会使所有临时条目都放置在 LDAP 树中的特定位置。临时子树 DN 是通过设置subtree-node属性来配置的。例如,如果subtree-nodeou=tempEntries并且条目的原始 DN 是cn=john doe, ou=users,则临时 DN 是cn=john doe, ou=tempEntries。请注意,配置的子树节点需要存在于 LDAP 树中。

在某些情况下DefaultTempEntryRenamingStrategy不起作用。例如,如果您打算进行递归删除,则需要使用DifferentSubtreeTempEntryRenamingStrategy. 这是因为递归删除操作实际上包括对子树中每个节点的深度优先删除。由于您不能重命名具有任何子节点的条目,并且DefaultTempEntryRenamingStrategy会将每个节点留在同一子树中(使用不同的名称)而不是实际删除它,因此此操作将失败。如有疑问,请使用DifferentSubtreeTempEntryRenamingStrategy.

13. 使用 Spring LDAP 进行用户认证

本节介绍使用 Spring LDAP 进行用户身份验证。它包含以下主题:

13.1. 基本认证

虽然 的核心功能ContextSource是提供DirContext实例供 使用LdapTemplate,但您也可以将其用于针对 LDAP 服务器对用户进行身份验证。的getContext(principal, credentials)方法ContextSource正是这样做的。DirContext它根据配置构造一个实例,ContextSource并使用提供的主体和凭据对上下文进行身份验证。自定义身份验证方法可能类似于以下示例:

public boolean authenticate(String userDn, String credentials) {
  DirContext ctx = null;
  try {
    ctx = contextSource.getContext(userDn, credentials);
    return true;
  } catch (Exception e) {
    // Context creation failed - authentication did not succeed
    logger.error("Login failed", e);
    return false;
  } finally {
    // It is imperative that the created DirContext instance is always closed
    LdapUtils.closeContext(ctx);
  }
}

userDn提供给方法的authenticateDN 必须是用户的完整 DN 才能进行身份验证(无论 上的base设置如何ContextSource)。您通常需要根据(例如)用户名执行 LDAP 搜索以获取此 DN。以下示例显示了如何执行此操作:

private String getDnForUser(String uid) {
  List<String> result = ldapTemplate.search(
      query().where("uid").is(uid),
      new AbstractContextMapper() {
         protected String doMapFromContext(DirContextOperations ctx) {
            return ctx.getNameInNamespace();
         }
      });

  if(result.size() != 1) {
    throw new RuntimeException("User not found or not unique");
  }

  return result.get(0);
}

这种方法有一些缺点。您被迫关心用户的 DN,您只能搜索用户的 uid,并且搜索始终从树的根(空路径)开始。更灵活的方法是让您指定搜索库、搜索过滤器和凭据。Spring LDAP 包括一个LdapTemplate提供此功能的身份验证方法:boolean authenticate(LdapQuery query, String password);.

当您使用此方法时,身份验证变得非常简单,如下所示:

示例 35. 使用 Spring LDAP 对用户进行身份验证
ldapTemplate.authenticate(query().where("uid").is("john.doe"), "secret");
下一节所述,某些设置可能需要您执行其他操作才能进行实际身份验证。有关详细信息,请参阅对已验证的上下文执行操作
不要编写自己的自定义身份验证方法。使用 Spring LDAP 中提供的那些。

13.2. 对已认证的上下文执行操作

一些身份验证方案和 LDAP 服务器需要对创建的实例执行一些操作才能进行DirContext实际身份验证。您应该测试并确保您的服务器设置和身份验证方案的行为方式。不这样做可能会导致用户被允许进入您的系统,而不管提供的 DN 和凭据如何。以下示例显示了一个验证方法的简单实现,其中lookup在经过验证的上下文上执行硬编码操作:

public boolean myAuthenticate(String userDn, String credentials) {
  DirContext ctx = null;
  try {
    ctx = contextSource.getContext(userDn, credentials);
    // Take care here - if a base was specified on the ContextSource
    // that needs to be removed from the user DN for the lookup to succeed.
    ctx.lookup(userDn);
    return true;
  } catch (Exception e) {
    // Context creation failed - authentication did not succeed
    logger.error("Login failed", e);
    return false;
  } finally {
    // It is imperative that the created DirContext instance is always closed
    LdapUtils.closeContext(ctx);
  }
}

如果操作可以作为回调接口的实现提供,而不是将操作限制为始终为lookup. Spring LDAP 包括AuthenticatedLdapEntryContextMapper回调接口和对应的authenticate方法:<T> T authenticate(LdapQuery query, String password, AuthenticatedLdapEntryContextMapper<T> mapper);

此方法允许对经过身份验证的上下文执行任何操作,如下所示:

示例 36. 使用 Spring LDAP 在经过身份验证的上下文上执行 LDAP 操作
AuthenticatedLdapEntryContextMapper<DirContextOperations> mapper = new AuthenticatedLdapEntryContextMapper<DirContextOperations>() {
  public DirContextOperations mapWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) {
    try {
      return (DirContextOperations) ctx.lookup(ldapEntryIdentification.getRelativeName());
    }
    catch (NamingException e) {
      throw new RuntimeException("Failed to lookup " + ldapEntryIdentification.getRelativeName(), e);
    }
  }
};

ldapTemplate.authenticate(query().where("uid").is("john.doe"), "secret", mapper);

13.3. 过时的身份验证方法

除了authenticate前面部分中描述的方法之外,您还可以使用许多不推荐使用的方法进行身份验证。虽然这些工作正常,但我们建议改用这些LdapQuery方法。

13.4. 使用 Spring Security

虽然前几节中描述的方法可能足以满足简单的身份验证场景,但该领域的需求通常会迅速扩展。许多方面都适用,包括身份验证、授权、Web 集成、用户上下文管理等。如果您怀疑要求可能超出简单的身份验证,您绝对应该考虑使用Spring Security来代替您的安全目的。它是一个功能齐全、成熟的安全框架,可以解决上述方面以及其他几个方面的问题。

14. LDIF 解析

LDAP 目录交换格式 (LDIF) 文件是用于以平面文件格式描述目录数据的标准介质。这种格式最常见的用途包括信息传输和存档。但是,该标准还定义了一种以平面文件格式描述对存储数据的修改的方法。后一种类型的 LDIF 通常称为changetypemodify LDIF。

org.springframework.ldap.ldif包提供了解析 LDIF 文件并将它们反序列化为有形对象所需的类。这LdifParserorg.springframework.ldap.ldif包的主要类,能够解析符合 RFC 2849 的文件。该类从资源中读取行并将它们组合成一个LdapAttributes对象。

当前LdifParser忽略changetype LDIF 条目,因为它们在应用程序上下文中的有用性尚未确定。

14.1. 对象表示

包中的两个类org.springframework.ldap.core提供了在代码中表示 LDIF 的方法:

  • LdapAttribute:扩展javax.naming.directory.BasicAttribute添加对 RFC2849 中定义的 LDIF 选项的支持。

  • LdapAttributes:扩展javax.naming.directory.BasicAttributes添加对 DN 的专门支持。

LdapAttribute对象将选项表示为Set<String>. 添加到LdapAttributes对象的 DN 支持使用javax.naming.ldap.LdapName该类。

14.2. 解析器

Parser接口为操作提供了基础,并采用了三个支持策略定义:

  • SeparatorPolicy:建立将行组合成属性的机制。

  • AttributeValidationPolicy:确保属性在解析之前结构正确。

  • Specification:提供一种机制,通过该机制可以在组装后验证对象结构。

这些接口的默认实现如下:

  • org.springframework.ldap.ldif.parser.LdifParser

  • org.springframework.ldap.ldif.support.SeparatorPolicy

  • org.springframework.ldap.ldif.support.DefaultAttributeValidationPolicy

  • org.springframework.ldap.schema.DefaultSchemaSpecification

这四个类一起逐行解析资源并将数据转换为LdapAttributes对象。

SeparatorPolicy决定了如何解释从源文件中读取的各个行,因为 LDIF 规范允许属性跨越多行。默认策略会根据阅读顺序对行进行评估,以确定所考虑行的性质。控制属性和更改类型记录被忽略。

使用DefaultAttributeValidationPolicyREGEX 表达式来确保每个属性在解析后符合有效的属性格式(根据 RFC 2849)。如果属性验证失败,InvalidAttributeFormatException则会记录 an 并跳过该记录(解析器返回null)。

14.3. 模式验证

Specification可以通过包中的接口获得针对模式验证已解析对象的机制org.springframework.ldap.schema。不进行任何验证,DefaultSchemaSpecification并且可用于已知记录有效且无需检查的情况。此选项可以节省验证强加的性能损失。应用基本检查,BasicSchemaSpecification例如确保已提供 DN 和对象类声明。目前,针对实际模式的验证需要实现Specification接口。

14.4. Spring 批量集成

虽然LdifParser任何需要解析 LDIF 文件的应用程序都可以使用它,但 Spring 提供了一个批处理框架,该框架提供了许多文件处理实用程序来解析分隔文件,例如 CSV。该org.springframework.ldap.ldif.batch包提供了使用LdifParserSpring Batch 框架中的有效配置选项所需的类。这个包中有五个类。它们共同提供了三个基本用例:

  • 从文件中读取 LDIF 记录并返回一个LdapAttributes对象。

  • 从文件中读取 LDIF 记录并将记录映射到 Java 对象 (POJO)。

  • 将 LDIF 记录写入文件。

第一个用例是用LdifReader. 这个类扩展了 Spring BatchAbstractItemCountingItemStreamItemReader并实现了它的ResourceAwareItemReaderItemStream. 它自然地适合框架,您可以使用它LdapAttributes从文件中读取对象。

您可以使用MappingLdifReader将 LDIF 对象直接映射到任何 POJO。此类要求您提供RecordMapper接口的实现。这个实现应该实现将对象映射到 POJO 的逻辑。

您可以实现RecordCallbackHandler并向任一读者提供实现。您可以使用此处理程序对跳过的记录进行操作。有关更多信息,请参阅Spring Batch API 文档

此包的最后一个成员LdifAggregator, 可用于将 LDIF 记录写入文件。此类调用对象的toString()方法LdapAttributes

15. 实用程序

本节介绍可以与 Spring LDAP 一起使用的其他实用程序。

15.1. 多值属性的增量检索

当特定属性有大量属性值 (>1500) 时,Active Directory 通常拒绝一次返回所有这些值。相反,属性值是根据多值属性的增量检索方法返回的。这样做需要调用部分检查返回的属性是否有特定的标记,并在必要时发出额外的查找请求,直到找到所有值。

Spring LDAPorg.springframework.ldap.core.support.DefaultIncrementalAttributesMapper在使用此类属性时会有所帮助,如下所示:

Object[] attrNames =  new Object[]{"oneAttribute", "anotherAttribute"};
Attributes attrs = DefaultIncrementalAttributeMapper.lookupAttributes(ldapTemplate, theDn, attrNames);

前面的示例解析任何返回的属性范围标记,并根据需要进行重复请求,直到检索到所有请求属性的所有值。

16. 测试

本节介绍使用 Spring LDAP 进行测试。它包含以下主题:

16.1. 使用嵌入式服务器

spring-ldap-test提供基于ApacheDSUnboundID的嵌入式 LDAP 服务器。

spring-ldap-test与 ApacheDS 1.5.5 兼容。不支持较新版本的 ApacheDS。

要开始,您需要包含spring-ldap-test依赖项。

以下清单显示了如何包含spring-ldap-testMaven:

<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-test</artifactId>
    <version>{project-version}</version>
    <scope>test</scope>
</dependency>

以下清单显示了如何包含spring-ldap-testfor Gradle:

testCompile "org.springframework.ldap:spring-ldap-test:{project-version}"

16.2. ApacheDS

要使用 ApacheDS,您需要包含许多 ApacheDS 依赖项。

以下示例显示如何为 Maven 包含 ApacheDS 依赖项:

<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-core</artifactId>
    <version>1.5.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-core-entry</artifactId>
    <version>1.5.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-protocol-shared</artifactId>
    <version>1.5.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-protocol-ldap</artifactId>
    <version>1.5.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-server-jndi</artifactId>
    <version>1.5.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.directory.shared</groupId>
    <artifactId>shared-ldap</artifactId>
    <version>0.9.15</version>
    <scope>test</scope>
</dependency>

以下示例显示了如何为 Gradle 包含 ApacheDS 依赖项:

testCompile "org.apache.directory.server:apacheds-core:1.5.5",
            "org.apache.directory.server:apacheds-core-entry:1.5.5",
            "org.apache.directory.server:apacheds-protocol-shared:1.5.5",
            "org.apache.directory.server:apacheds-protocol-ldap:1.5.5",
            "org.apache.directory.server:apacheds-server-jndi:1.5.5",
            "org.apache.directory.shared:shared-ldap:0.9.15"

以下 bean 定义创建一个嵌入式 LDAP 服务器:

<bean id="embeddedLdapServer" class="org.springframework.ldap.test.EmbeddedLdapServerFactoryBean">
    <property name="partitionName" value="example"/>
    <property name="partitionSuffix" value="dc=261consulting,dc=com" />
    <property name="port" value="9321" />
</bean>

spring-ldap-test提供了一种机制来使用org.springframework.ldap.test.LdifPopulator. 要使用它,请创建一个类似于以下内容的 bean:

<bean class="org.springframework.ldap.test.LdifPopulator" depends-on="embeddedLdapServer">
    <property name="contextSource" ref="contextSource" />
    <property name="resource" value="classpath:/setup_data.ldif" />
    <property name="base" value="dc=jayway,dc=se" />
    <property name="clean" value="true" />
    <property name="defaultBase" value="dc=jayway,dc=se" />
</bean>

另一种处理嵌入式 LDAP 服务器的方法是使用org.springframework.ldap.test.TestContextSourceFactoryBean,如下所示:

<bean id="contextSource" class="org.springframework.ldap.test.TestContextSourceFactoryBean">
    <property name="defaultPartitionSuffix" value="dc=jayway,dc=se" />
    <property name="defaultPartitionName" value="jayway" />
    <property name="principal" value="uid=admin,ou=system" />
    <property name="password" value="secret" />
    <property name="ldifFile" value="classpath:/setup_data.ldif" />
    <property name="port" value="1888" />
</bean>

此外,org.springframework.ldap.test.LdapTestUtils还提供了以编程方式使用嵌入式 LDAP 服务器的方法。

16.3. 未绑定ID

要使用 UnboundID,您需要包含 UnboundID 依赖项。

以下示例显示了如何为 Maven 包含 UnboundID 依赖项:

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.1.1</version>
    <scope>test</scope>
</dependency>

以下示例显示了如何为 Gradle 包含 UnboundID 依赖项:

testCompile "com.unboundid:unboundid-ldapsdk:3.1.1"

以下 bean 定义创建一个嵌入式 LDAP 服务器:

<bean id="embeddedLdapServer" class="org.springframework.ldap.test.unboundid.EmbeddedLdapServerFactoryBean">
    <property name="partitionName" value="example"/>
    <property name="partitionSuffix" value="dc=261consulting,dc=com" />
    <property name="port" value="9321" />
</bean>

spring-ldap-test提供了一种使用org.springframework.ldap.test.unboundid.LdifPopulator. 要使用它,请创建一个类似于以下内容的 bean:

<bean class="org.springframework.ldap.test.unboundid.LdifPopulator" depends-on="embeddedLdapServer">
    <property name="contextSource" ref="contextSource" />
    <property name="resource" value="classpath:/setup_data.ldif" />
    <property name="base" value="dc=jayway,dc=se" />
    <property name="clean" value="true" />
    <property name="defaultBase" value="dc=jayway,dc=se" />
</bean>

另一种处理嵌入式 LDAP 服务器的方法是使用org.springframework.ldap.test.unboundid.TestContextSourceFactoryBean. 要使用它,请创建一个类似于以下内容的 bean:

<bean id="contextSource" class="org.springframework.ldap.test.unboundid.TestContextSourceFactoryBean">
    <property name="defaultPartitionSuffix" value="dc=jayway,dc=se" />
    <property name="defaultPartitionName" value="jayway" />
    <property name="principal" value="uid=admin,ou=system" />
    <property name="password" value="secret" />
    <property name="ldifFile" value="classpath:/setup_data.ldif" />
    <property name="port" value="1888" />
</bean>

此外,org.springframework.ldap.test.unboundid.LdapTestUtils提供以编程方式使用嵌入式 LDAP 服务器的方法。


1. see XML Configuration