# Spring LDAP 参考

## 1. 前言

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

* 它们需要大量的管道代码，即使是执行最简单的任务也是如此。

* 无论发生什么，所有的资源都需要被正确地关闭。

* 异常处理是困难的。

在 API 的常见用例中，这些点通常会导致大量的代码重复。众所周知，代码复制是最糟糕的“代码气味”之一。总而言之，归结起来就是：Java 中的 JDBC 和 LDAP 编程都非常枯燥且重复。

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

## 2. 导言

这一部分提供了对 LDAP 的相对快速的介绍。

### 2.1.概述

Spring LDAP 旨在简化 Java 中的 LDAP 编程。该图书馆提供的一些功能包括：

* [`JdbcTemplate`](https://DOCS. Spring.io/ Spring/DOCS/current/javadoc-api/org/springframework/jdbc/core/jdbctemplate.html) -风格的模板简化到 LDAP 编程。

* JPA-或 Hibernate-风格的基于对象和目录映射的注释。

* Spring 数据存储库支持，包括对 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 LDAP`AttributesMapper`和`LdapTemplate`类，我们获得了与以下代码完全相同的功能：

```
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();
            }
         });
   }
}
```

样板代码的数量比传统示例中的要少得多。`LdapTemplate`搜索方法确保创建一个`DirContext`实例，执行搜索，通过使用给定的`AttributesMapper`将属性映射到字符串，在内部列表中收集字符串，最后返回列表。它还确保`NamingEnumeration`和`DirContext`适当地闭合，并处理可能发生的任何异常。

自然地，这是一个 Spring 框架子项目，我们使用 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](https://github.com/spring-projects/spring-ldap/milestone/21?closed=1)的变更日志。 Spring LDAP2.2 的重点如下：

* [\#415](https://github.com/spring-projects/spring-ldap/issues/415):增加了对 Spring 5 的支持

* [\#399](https://github.com/spring-projects/spring-ldap/pull/399):嵌入式 Unboundid LDAP 服务器支持

* [\#410](https://github.com/spring-projects/spring-ldap/pull/410):为 Commons Pool2 支持添加了文档

### 2.4.最新更新在 2.1 中

有关 2.1 的完整详细信息，请参见[2.1.0.RC1](https://github.com/spring-projects/spring-ldap/issues?q=milestone%3A2.1.0.RC1)和[2.1.0](https://github.com/spring-projects/spring-ldap/issues?utf8=%E2%9C%93&q=milestone%3A2.1.0)的变更日志 Spring LDAP2.1 的重点如下。

* [\#390](https://github.com/spring-projects/spring-ldap/pull/390):添加了 Spring 数据跳转支持

* [\#351](https://github.com/spring-projects/spring-ldap/issues/351):增加了对 Commons-Pool2 的支持

* [\#370](https://github.com/spring-projects/spring-ldap/issues/370):在 XML 命名空间中添加了支持属性占位符

* [\#392](https://github.com/spring-projects/spring-ldap/pull/392):添加了文档测试支持

* [\#401](https://github.com/spring-projects/spring-ldap/pull/401):添加了一个切换到 AssertJ

* 从 JIRA 迁移到[GitHub 问题](https://github.com/spring-projects/spring-ldap/issues)

* 新增[Gitter Chat](https://gitter.im/spring-projects/spring-ldap)

### 2.5.最新更新在 2.0 中

虽然在版本 2.0 中对 Spring LDAP API 进行了相当重要的现代化，但已经非常小心地确保尽可能地向后兼容。使用 Spring LDAP1.3.x 的代码在使用 2.0 库时，不需要做任何修改，就应该编译并运行，只有很少的例外情况。

例外情况是，为了使几个重要的重构成为可能，少数类被移到了新的包中。移动的类通常不是预期的公共 API 的一部分，迁移过程应该是平稳的。每当升级后找不到 Spring LDAP 类时，你应该在 IDE 中组织导入。

不过，你应该会遇到一些不推荐警告，而且还有很多其他的 API 改进。对于尽可能多地退出 2.0 版本的建议是，远离不受欢迎的类和方法，并迁移到新的、改进的 API 实用程序。

下面的列表简要描述了 Spring LDAP2.0 中最重要的变化：

* Spring LDAP 现在需要 Java6。 Spring 仍然支持 2.0 及以上版本。

* 中央 API 已经更新了 Java5+ 功能，如泛型和 varargs。因此，整个`spring-ldap-tiger`模块已被弃用，我们鼓励你迁移到使用核心 Spring LDAP 类。核心接口的参数化会在现有代码上导致大量的编译警告，我们鼓励你采取适当的措施来消除这些警告。

* ODM（对象-目录映射）功能已移至 Core，并且在`LdapOperations`和`LdapTemplate`中有新的方法，它们使用此自动转换到和从 ODM 注释的类。有关更多信息，请参见[[ODM]]。

* 现在（最终）提供了一个定制的 XML 命名空间，以简化 Spring LDAP 的配置。有关更多信息，请参见[[configuration]（#configuration）。

* Spring LDAP 现在提供对 Spring 数据存储库和 QueryDSL 的支持。有关更多信息，请参见[[repositories]]。

* 在`DirContextAdapter`和 ODM 中，作为属性值的`Name`实例现在可以正确地处理有关可区分名称相等的问题。有关更多信息，请参见[[dns-as-attribute-values]和[[odm-dn-attributes]]。

* `DistinguishedName`和相关的类已被弃用，而支持标准的 Java`LdapName`。有关库在使用`LdapName`对象时如何提供帮助的信息，请参见[动态构建专有名称](#ldap-names)。

* Fluent LDAP 查询构建支持已添加。这使得在 Spring LDAP 中使用 LDAP 搜索时获得更愉快的编程体验。有关 LDAP 查询生成器支持的更多信息，请参见[构建 LDAP 查询](#basic-queries)和[[query-builder-advanced]]。

* 在`LdapTemplate`中，旧的`authenticate`方法已被弃用，取而代之的是一些新的`authenticate`方法，这些方法可在身份验证失败时使用`LdapQuery`对象和*抛出异常*对象，从而使用户更容易地找出导致身份验证尝试失败的原因。

* 对[samples](https://github.com/spring-projects/spring-ldap/tree/main/samples)进行了改进和更新，以利用 2.0 中的功能。在提供一个[LDAP 用户管理应用程序](https://github.com/spring-projects/spring-ldap/tree/main/samples/user-admin)的有用示例方面已经付出了相当大的努力。

### 2.6.包装概述

至少，要使用 Spring LDAP，你需要以下条件：

* `spring-ldap-core`: Spring LDAP 库

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

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

* `spring-data-commons`:存储库支持等的基本基础设施

* `slf4j`:一个简单的日志记录门面，内部使用

除了所需的依赖关系外，某些功能还需要以下可选的依赖关系：

* `spring-context`:如果你的应用程序是通过使用 Spring 应用程序上下文连接起来的，则需要。`spring-context`增加了应用程序对象通过使用一致的 API 获得资源的能力。如果你计划使用`BaseLdapPathBeanPostProcessor`，则肯定需要它。

* `spring-tx`:如果你计划使用客户端补偿事务支持，则需要。

* `spring-jdbc`:如果你计划使用客户端补偿事务支持，则需要。

* `commons-pool`:如果你计划使用池功能，则需要。

* `spring-batch`:如果你计划将 LDIF 解析功能与 Spring 批处理一起使用，则需要此功能。

### 2.7.开始

[samples](https://github.com/spring-projects/spring-ldap/tree/main/samples)提供了一些关于如何将 Spring LDAP 用于常见用例的有用示例。

### 2.8.支持

如果你有问题，请在[stack overflow with`spring-ldap`tag]（https://stackoverflow.com/questions/tagged/ Spring-ldap）上向他们提问。该项目的网页是[https://spring.io/spring-ldap/](https://spring.io/spring-ldap/)。

### 2.9.鸣谢

启动 Spring LDAP 项目时的初始工作是由[Jayway](https://www.jayway.com)发起的。该项目目前的维护由[Pivotal](https://pivotal.io)提供资金，该资金已由[VMware](https://vmware.com)获得。

感谢[结构 101](https://structure101.com/)提供了一个开放源码许可，它可以用来控制项目结构。

## 3. 基本用法

本节描述了使用 Spring LDAP 的基础知识。它包含以下内容：

* [使用`AttributesMapper`进行搜索和查找]（# Spring-ldap-basic-usage-search-lookup-attributesmapper）

* [构建 LDAP 查询](#basic-queries)

* [动态构建专有名称](#ldap-names)

* [Examples](#spring-ldap-basic-usage-examples)

* [有约束力和无约束力](#spring-ldap-basic-usage-binding-unbinding)

* [Updating](#spring-ldap-basic-usage-updating)

### 3.1.使用`AttributesMapper`搜索和查找

下面的示例使用[`AttributesMapper`]（https://DOCS. Spring.io/ Spring-ldap/DOCS/current/apidocs/org/springframework/ldap/core/attributesmapper.html）来构建所有 Person 对象的所有通用名称的列表。

例 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，你可以直接检索该条目，而无需搜索它。这在 Java LDAP 中被称为“查找”。下面的示例显示了`Person`对象的查找：

例 3。查找人形物体的一种方法

```
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`]（https://DOCS. Spring.io/ Spring-ldap/DOCS/current/apidocs/org/springframework/ldap/query/ldapquerybuilder.html），其中包含用于构建 LDAP 查询的 Fluent API。

假设你想要执行一个从基本 DN`dc=261consulting,dc=com`开始的搜索，将返回的属性限制为`cn`和`sn`，并使用`(&(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();
            }
         });
   }
}
```

|   |除了简化复杂搜索参数的构建，`LdapQueryBuilder`及其相关类还提供了对搜索过滤器中任何不安全字符的正确转义。这防止了“LDAP 注入”，在这种情况下，用户可能会使用这样的字符将不需要的操作注入到你的 LDAP 操作中。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |`LdapTemplate`包含许多用于执行 LDAP 搜索的重载方法。这是为了适应尽可能多的不同用例和编程风格首选项。对于绝大多数用例，推荐使用的方法是以`LdapQuery`作为输入的方法。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |`AttributesMapper`只是处理搜索和查找数据时可以使用的可用回调接口之一。参见[使用`DirContextAdapter`简化属性访问和操作]以获得替代方案。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

有关`LdapQueryBuilder`的更多信息，请参见[[query-builder-advanced]]。

### 3.3.动态构建专有名称

标准的 Java 实现的专有名称（[`LdapName`]（https://DOCS.oracle.com/javase/6/DOCS/api/javax/naming/ldap/ldapname.html））在解析专有名称时表现良好。然而，在实际使用中，这种实现方式有一些缺点：

* `LdapName`实现是可变的，非常适合表示标识的对象。

* 尽管它具有可变的性质，但是通过使用`LdapName`动态构建或修改专有名称的 API 非常麻烦。提取索引或（特别是）命名组件的值也有点困难。

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

为了简化使用专有名称的工作， Spring LDAP 提供了[`LdapNameBuilder`]（https://DOCS. Spring.io/ Spring-ldap/DOCS/current/apDOCS/org/springframework/ldap/support/ldapnamebuilder.html），以及[`LdapUtils`]（https://DOCS. Spring.io/ Spring-ldap/docs/docs/current/ldapframework/ls/lsuptils.html）中的许多实用方法，这些方法在使用<<>r=“180”/>r=" 时提供了帮助。

#### 3.3.1.例子

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

例 5。通过使用`LdapNameBuilder`动态构建`LdapName`

```
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();
  }
  ...
}
```

假设`Person`具有以下属性：

|Attribute Name|属性值|
|--------------|---------------|
|  `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 LDAP1.x 提供了自己的实现，`DistinguishedName`。该实现本身存在一些缺陷，在 2.0 版本中已被弃用。现在你应该使用`LdapName`以及前面描述的实用程序。

### 3.4.有约束力和无约束力

本节介绍如何添加和删除数据。更新包含在[下一节](#spring-ldap-basic-usage-updating)中。

#### 3.4.1.添加数据

在 Java LDAP 中插入数据称为绑定。这有点令人困惑，因为在 LDAP 术语中，“bind”的意思完全不同。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 Unbind 执行 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.使用 rebind 进行更新

a`rebind`是一种粗略的修改数据的方法。它基本上是一个`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});
   }
}
```

构建`Attributes`和`ModificationItem`数组是一项大量的工作。然而，正如我们在[用`DirContextAdapter`简化属性访问和操作]中描述的那样， Spring LDAP 为简化这些操作提供了更多帮助。

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

Java LDAP API 的一个鲜为人知（也可能被低估了）的特性是能够注册`DirObjectFactory`，从而从找到的 LDAP 条目中自动创建对象。 Spring LDAP 利用该特性在某些搜索和查找操作中返回[`DirContextAdapter`]（https://DOCS. Spring.io/ Spring-ldap/DOCS/current/apidocs/org/SpringFramework/ldap/core/dircontextadapter.html）实例。

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

### 4.1.使用`ContextMapper`搜索和查找

只要在 LDAP 树中找到一个条目， Spring LDAP 就会使用它的属性和可区分名称来构造`DirContextAdapter`。这使得我们可以使用[`ContextMapper`]（https://DOCS. Spring.io/ Spring-ldap/DOCS/current/apidocs/org/springframework/ldap/core/contextmapper.html）来代替`AttributesMapper`来转换所发现的值，如下所示：

例 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());
   }
}
```

如前面的示例所示，我们可以直接通过名称检索属性值，而不必通过`Attributes`和`Attribute`类。这在处理多值属性时特别有用。从多值属性提取值通常需要循环使用从`Attributes`实现返回的属性值`NamingEnumeration`。`DirContextAdapter`在[`getStringAttributes()`]（https://DOCS. Spring.io/ Spring.io/ldap/DOCS/current/apiDOCS/org/org/springframework/ldap/ldap/core/core/dirtcontextaptapter.html#getstringstringtributions）或[java.lang.langstringstringtristributes=“/>[detributries=”/>（[dapps:////下面的示例使用`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`]（https://DOCS. Spring.io/ Spring-ldap/DOCS/current/apidocs/org/springframework/ldap/core/support/abstractcontextmapper.html）。此实现自动将提供的`Object`参数转换为`DirContexOperations`。使用`AbstractContextMapper`，可以将前面显示的`PersonContextMapper`重写如下：

例 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`添加和更新数据

` While useful when extracting attribute values, `dircontextadapter` 在管理添加和更新数据所涉及的细节方面甚至更加强大。

#### 4.2.1.使用`DirContextAdapter`添加数据

下面的示例使用`DirContextAdapter`来实现`create`中呈现的[添加数据](#basic-binding-data)存储库方法的改进实现：

例 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`，因为我们没有显式地指定属性。

还请注意在设置`objectclass`属性值时使用`setAttributeValues()`方法。`objectclass`属性是多值的。与提取多值属性数据的困难类似，构建多值属性是一项繁琐而繁琐的工作。通过使用`setAttributeValues()`方法，可以让`DirContextAdapter`句柄为你工作。

#### 4.2.2.使用`DirContextAdapter`更新数据

我们以前看到，使用`modifyAttributes`进行更新是推荐的方法，但是这样做需要我们执行计算属性修改并相应地构造`ModificationItem`数组的任务。`DirContextAdapter`可以为我们完成所有这些工作，如下所示：

使用`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);
   }
}
```

当不向`ldapTemplate.lookup()`传递映射器时，结果是`DirContextAdapter`实例。当`lookup`方法返回`Object`时，`lookupContext`便利方法方法自动将返回值强制转换为`DirContextOperations`（`DirContextAdapter`实现的接口）。

注意，我们在`create`和`update`方法中有重复的代码。这段代码从域对象映射到上下文。可以将其提取为单独的方法，如下所示：

```
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());
   }
}
```

\===`DirContextAdapter`和专有名称作为属性值

在 LDAP 中管理安全组时，通常使用表示可区分名称的属性值。由于可分辨名称相等与字符串相等不同（例如，在可分辨名称相等中忽略空格和大小写的差异），因此使用字符串相等计算属性修改不能像预期的那样工作。

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

在 Spring LDAP2.0 中，向属性修改方法提供`javax.naming.Name`实例使得`DirContextAdapter`在计算属性修改时使用专有名称相等。如果我们将较早的示例修改为`ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People"))`，那么它确实会**不是**呈现一个修改，如下例所示：

```
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`以获得[[base-context-configuration]中描述的基本 LDAP 路径。这是必要的，因为作为成员属性值的专有名称必须始终是绝对的，来自目录根。

\=== 一个完整的`PersonRepository`类

为了说明 Spring LDAP 和`DirContextAdapter`的有用性，下面的示例展示了用于 LDAP 的完整的`Person`存储库实现：

```
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;
      }
   }
}
```

|   |在几种情况下，对象的可分辨名称是通过使用对象的属性来构造的，<br/>在前面的示例中，在 DN 中使用`Person`的国家、公司和全称，这意味着，更新这些属性中的任何一个实际上都需要使用`rename()`操作来移动 LDAP 树中的条目，此外还需要更新`Attribute`值，<br/>由于这是高度特定于实现的，因此你需要对此进行跟踪，通过不允许用户更改这些属性，或者根据需要在你的`update()`方法中执行`rename()`操作。<br/>注意，通过使用[[odm]]，如果你适当地注释域类，库可以自动为你处理此问题。|
|---| |

\== 对象-目录映射

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

* `<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)`

\=== 注释

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

* `@Entry`:类级别注释，指示实体映射到的`objectClass`定义。*（要求）*

* `@Id`:表示实体 DN。声明此属性的字段必须是`javax.naming.Name`类的导数。（要求）

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

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

* `@Transient`:表示字段不是持久性的，应该被`OdmManager`忽略。

`@Entry`和`@Id`注释需要在托管类上声明。`@Entry`用于指定实体映射到哪个对象类，以及（可选地）由类表示的 LDAP 条目的目录根。需要声明映射字段的所有对象类。请注意，在创建托管类的新条目时，仅使用声明的对象类。

为了使目录条目被认为与所管理的实体匹配，目录条目声明的所有对象类都必须用`@Entry`注释声明。例如，假设你的 LDAP 树中的条目具有以下对象类：`inetOrgPerson,organizationalPerson,person,top`。如果你只对更改`person`对象类中定义的属性感兴趣，则可以用`@Entry`注释`@Entry(objectClasses = { "person", "top" })`。但是，如果希望管理`inetOrgPerson`ObjectClass 中定义的属性，则需要使用以下方法：`@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" })`。

`@Id`注释用于将条目的专有名称映射到字段。字段必须是`javax.naming.Name`的实例。

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

`@DnAttribute`注释用于将对象类字段映射到条目的专有名称中的组件之间。当从目录树中读取条目时，用`@DnAttribute`注释的字段会自动地从专有名称中填充适当的值。只有类型`String`的字段可以用`@DnAttribute`进行注释。不支持其他类型。如果指定了类中所有`@DnAttribute`注释的`index`属性，则在创建和更新条目时也可以自动计算 DN。对于更新场景，如果专有名称的一部分属性发生了更改，这也会自动处理树中的移动项。

`@Transient`注释表示对象目录映射应该忽略该字段，而不是映射到底层的 LDAP 属性。请注意，如果`@DnAttribute`不绑定到`Attribute`。也就是说，它只是专有名称的一部分，不是由对象属性表示的。它还必须用`@Transient`进行注释。

\=== 执行

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

```
@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);
   }
}
```

\===ODM 和专有名称作为属性值

LDAP 中的安全组通常包含一个 multi-value 属性，其中每个值都是系统中用户的可识别名称。在[[[dns-as-attribute-values]]中讨论了处理这类属性时所涉及的困难。

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

```
@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`、`addMember`和`removeMember`来修改组成员，然后调用`ldapTemplate.update()`时，属性修改将通过使用可分辨名称相等性来计算，这意味着在计算可分辨名称是否相等时，将忽略可分辨名称的文本格式。

\== 高级 LDAP 查询

本节介绍了如何在 Spring LDAP 中使用 LDAP 查询的各种方法。

\===LDAP 查询生成器参数

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

* `base`:在 LDAP 树中指定搜索应该从哪里开始的根 DN。

* `searchScope`:指定搜索应该遍历到 LDAP 树的深度。

* `attributes`:指定从搜索返回的属性。默认值就是全部。

* `countLimit`:指定从搜索中返回的条目的最大数量。

* `timeLimit`:指定搜索所需的最大时间。

* 搜索过滤器：我们正在寻找的条目必须满足的条件。

通过调用`LdapQueryBuilder`的`query`方法创建`LdapQueryBuilder`。它的目的是作为 Fluent Builder API，首先定义基本参数，然后是过滤器规范调用。一旦过滤器条件已经开始定义，并调用`where`的`LdapQueryBuilder`方法，以后调用`base`的尝试（例如）将被拒绝。基本搜索参数是可选的，但至少需要一个过滤器规范调用。下面的查询搜索对象类`Person`的所有条目：

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

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

下面的查询搜索对象类`person`和`cn`（通用名称）`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`（域组件）开始的所有条目：

```
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());
```

下面的查询为所有具有`person`对象类并以`dc`（域组件）`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`）：

```
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());
```

\=== 筛选条件

前面的示例演示了 LDAP 过滤器中的简单等条件。LDAP 查询生成器支持以下条件类型：

* `is`:指定 equals（=）条件。

* `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))`

\=== 硬编码过滤器

在某些情况下，你可能希望指定一个硬编码的过滤器作为`LdapQuery`的输入。`LdapQueryBuilder`为此目的有两种方法：

* `filter(String hardcodedFilter)`:使用指定的字符串作为筛选器。请注意，指定的输入字符串不会以任何方式被触及，这意味着如果你正在从用户输入构建过滤器，那么此方法并不特别适合。

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

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

不能将硬编码的过滤器方法与前面描述的`where`方法混合使用。这是非此即彼。如果你使用`filter()`指定过滤器，那么如果你之后尝试调用`where`，将会出现异常。

\== 配置

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">
```

\===`ContextSource`配置

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

```
<ldap:context-source
    username="cn=Administrator"
    password="secret"
    url="ldap://localhost:389" />
```

前面的示例创建了一个`LdapContextSource`，它具有默认值（请参见本段后面的表格）以及指定的 URL 和身份验证凭据。上下文源上的可配置属性如下（需要标记为 \* 的属性）：

|          Attribute          |                      Default                       |说明|
|-----------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|            `id`             |                  `contextSource`                   |被创建者的 ID Bean。|
|         `username`          |                                                    |在使用 LDAP 服务器进行身份验证时使用的用户名。<br/>这通常是管理用户（例如，`cn=Administrator`）的专有名称，但可能会根据服务器和身份验证方法而有所不同。<br/>如果`authentication-source-ref`未显式配置，则需要。|
|         `password`          |                                                    |使用 LDAP 服务器进行身份验证时要使用的密码（凭据）。如果`authentication-source-ref`未显式配置，则需要。|
|          `url` \*           |                                                    |要使用的 LDAP 服务器的 URL。URL 应该是以下格式：`ldap://myserver.example.com:389`。<br/>对于 SSL 访问，使用`ldaps`协议和适当的端口—例如，`ldaps://myserver.example.com:636`。<br/>如果你想要故障转移功能，可以指定一个以上的 URL，用逗号分隔（`,`）。|
|           `base`            |            `LdapUtils.emptyLdapName()`             |基础 DN。当此属性被配置后，所有提供给 LDAP 操作并从 LDAP 操作接收到的专有名称都是相对于指定的 LDAP 路径的。<br/>这可以大大简化针对 LDAP 树的工作。但是，有几种情况下你需要访问基本路径。<br/>有关此的详细信息，请参见[[base-context-configuration]（#base-context-configuration）|
|    `anonymous-read-only`    |                      `false`                       |定义是否通过使用匿名（未经验证）上下文执行只读操作。<br/>注意，将此参数设置为`true`以及补偿事务支持不受支持，因此将被拒绝。|
|         `referral`          |                       `null`                       |定义处理推荐的策略，如[here](https://docs.oracle.com/javase/jndi/tutorial/ldap/referral/jndi.html)所述。有效值为：<br/><br/>*`ignore`<br/><br/>*`follow`<br/><br/>*`throw`|
|      `native-pooling`       |                      `false`                       |指定是否使用本机 Java LDAP 连接池。考虑使用 Spring LDAP 连接池。有关更多信息，请参见[[pooling]]。|
| `authentication-source-ref` |      A `SimpleAuthenticationSource` instance.      |要使用的`AuthenticationSource`实例的 ID（参见[[[ Spring-ldap-custom-principal-creditions-management]]]（# Spring-ldap-custom-principal-creditions-management）））。|
|`authentication-strategy-ref`|A `SimpleDirContextAuthenticationStrategy` instance.|要使用的`DirContextAuthenticationStrategy`实例的 ID（参见[[[ Spring-ldap-custom-dircontext-assignation-processing]]]（# Spring-ldap-custom-dircontext-assignation-processing）））。|
|    `base-env-props-ref`     |                                                    |对`Map`的自定义环境属性的引用，该环境属性应随环境一起提供，并在构造时发送到`DirContext`。|

\====`DirContext`身份验证

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

|   |本节引用`ContextSource`核心功能中的验证上下文，以构造`DirContext`实例，供`LdapTemplate`使用。LDAP 通常仅用于用户身份验证，`ContextSource`也可用于此目的。该过程将在[[User-Authentication]]中讨论。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

默认情况下，为只读和读写操作创建经过验证的上下文。你应该指定 LDAP 用户的`username`和`password`用于`context-source`元素上的身份验证。

|   |如果`username`是 LDAP 用户的专有名称，则它需要是来自 LDAP 树的根的用户的完整 DN，无论是否已在`context-source`元素上指定了`base`LDAP 路径。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

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

\====== 自定义`DirContext`身份验证处理

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

在许多情况下，这种处理是不够的。例如，LDAP 服务器通常被设置为仅接受安全 TLS 通道上的通信。可能需要使用特定的 LDAP 代理验证机制或其他问题。

可以通过向`context-source`元素提供`DirContextAuthenticationStrategy`实现引用来指定替代的身份验证机制。要做到这一点，请设置`authentication-strategy-ref`属性。

\=====TLS

Spring LDAP 为需要 TLS 安全信道通信的 LDAP 服务器提供了两种不同的配置选项：`DefaultTlsDirContextAuthenticationStrategy`和`ExternalTlsDirContextAuthenticationStrategy`。这两种实现都在目标连接上协商 TLS 通道，但它们在实际的身份验证机制中有所不同。其中`DefaultTlsDirContextAuthenticationStrategy`在安全通道上应用简单的身份验证（通过使用指定的`username`和`password`），`ExternalTlsDirContextAuthenticationStrategy`使用外部 SASL 身份验证，应用通过使用系统属性进行身份验证而配置的客户端证书。

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

|   |在使用 TLS 连接时，需要确保关闭本机 LDAP 池功能（通过使用`native-pooling`属性指定）。如果`shutdownTlsGracefully`被设置为`false`，这一点尤其重要。然而，由于 TLS 通道协商过程非常昂贵，因此可以通过使用 Spring LDAP 池支持来获得很好的性能优势，如[[池]中所述。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

\===== 自定义主体和凭据管理

默认情况下，用于创建经过验证的`Context`的用户名（即用户 DN）和密码是静态定义的（在`context-source`元素配置中定义的那些在`ContextSource`的整个生命周期中使用），有几种情况下，这不是理想的行为。一个常见的场景是，在为该用户执行 LDAP 操作时，应该使用当前用户的主体和凭据。通过使用`authentication-source-ref`元素，而不是显式地指定`username`和`password`元素，向`context-source`元素提供对`AuthenticationSource`实现的引用，可以修改默认行为。每次创建经过身份验证的`Context`时，`AuthenticationSource`都会通过`ContextSource`查询主体和凭据。

如果使用[Spring Security](https://spring.io/spring-security)，则可以通过使用 Spring Security 附带的`SpringSecurityAuthenticationSource`实例配置你的`ContextSource`，来确保当前登录用户的主体和凭据一直被使用。下面的示例展示了如何做到这一点：

```
<beans>
...
    <ldap:context-source
        url="ldap://localhost:389"
        authentication-source-ref="springSecurityAuthenticationSource"/>

    <bean id="springSecurityAuthenticationSource"
        class="org.springframework.security.ldap.authentication.SpringSecurityAuthenticationSource" />
...
</beans>
```

|   |当使用`AuthenticationSource`时，对于.our`context-source`，我们不指定任何`username`或`password`。只有在使用默认行为时才需要这些属性。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |当使用`SpringSecurityAuthenticationSource`时，你需要使用 Spring Security 的`LdapAuthenticationProvider`来针对 LDAP 对用户进行身份验证。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------|

\==== 原生 Java LDAP 池

内部 Java LDAP 提供程序提供了一些非常基本的池功能。你可以使用`AbstractContextSource`上的`pooled`标志来打开或关闭这个 LDAP 连接池。默认值是`false`（自版本 1.3 以来）——也就是说，本机 Java LDAP 池已关闭。LDAP 连接池的配置是通过使用`System`属性来管理的，因此你需要在 Spring 上下文配置之外手动处理此问题。你可以找到本机池配置[here](https://java.sun.com/products/jndi/tutorial/ldap/connect/config.html)的详细信息。

|   |在内置的 LDAP 连接池中存在几个严重的缺陷，这就是为什么 Spring LDAP 提供了一种更复杂的 LDAP 连接池方法的原因，在[[[pooling]]（#pooling）中进行了描述。如果你需要池功能，这是推荐的方法。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |不管池配置如何，`ContextSource#getContext(String principal, String credentials)`方法总是显式地不使用原生 Java LDAP 池，以使重置密码尽快生效。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

\====Advanced`ContextSource`配置

本节介绍了配置`ContextSource`的更高级的方法。

\====== 自定义`DirContext`环境属性

在某些情况下，除了在`context-source`上可直接配置的属性外，你可能还需要指定其他的环境设置属性。你应该在`Map`中设置这样的属性，并在`base-env-props-ref`属性中引用它们。

\===`LdapTemplate`配置

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

```
<ldap:ldap-template />
```

元素本身创建一个带有默认 ID 的`LdapTemplate`实例，引用默认的`ContextSource`，该实例的 ID 预计为`contextSource`（`context-source`元素的默认 ID）。

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

|       Attribute       |    Default    |说明|
|-----------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|         `id`          |`ldapTemplate` |被创建者的 ID Bean。|
| `context-source-ref`  |`contextSource`|要使用的`ContextSource`实例的 ID。|
|     `count-limit`     |      `0`      |搜索的默认计数限制。0 表示没有限制。|
|     `time-limit`      |      `0`      |默认的搜索时间限制，以毫秒为单位。0 表示没有限制。|
|    `search-scope`     |   `SUBTREE`   |搜索的默认搜索范围。有效值为：<br/><br/>*`OBJECT`<br/><br/>*`ONELEVEL`<br/><br/>*`SUBTREE`|
|`ignore-name-not-found`|    `false`    |指定在搜索中是否应忽略`NameNotFoundException`。将此属性设置为`true`，将无效的搜索库导致的错误静默地删除。|
|`ignore-partial-result`|    `false`    |指定在搜索中是否应忽略`PartialResultException`。一些 LDAP 服务器在推荐方面存在问题。这些通常应该自动遵循。但是，如果这不起作用，则它以`PartialResultException`表示自己。将此属性设置为`true`可解决此问题。|
|       `odm-ref`       |               |要使用的`ObjectDirectoryMapper`实例的 ID。默认值是默认配置的`DefaultObjectDirectoryMapper`。|

\=== 获取对基本 LDAP 路径的引用

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

出于这个原因， Spring LDAP 具有一种机制，通过该机制，任何 Spring 控制的 Bean 都可以在启动时提供基本路径。对于要通知 bean 的基本路径，有两件事需要到位。首先，需要基本路径引用的 Bean 需要实现`BaseLdapNameAware`接口。其次，需要在应用程序上下文中定义`BaseLdapPathBeanPostProcessor`。下面的示例展示了如何实现`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();
   }
   ...
}
```

下面的示例展示了如何定义`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`的默认行为是在`ApplicationContext`中使用单个定义的`BaseLdapPathSource`（`AbstractContextSource`）的基本路径。如果定义了多个`BaseLdapPathSource`，则需要通过设置`baseLdapPathSourceName`属性来指定使用哪个。

\== Spring LDAP 存储库

Spring LDAP 具有对 Spring 数据存储库的内置支持。描述了基本的功能和配置[here](https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#repositories)。在使用 Spring LDAP 存储库时，你应该记住以下几点：

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

* 要在自动生成的存储库中包含对`LdapQuery`参数的支持，请让你的接口扩展`LdapRepository`，而不是`CrudRepository`。

* 所有 Spring LDAP 存储库都必须与带有 ODM 注释的实体一起工作，如[[ODM]]中所述。

* 由于所有的 ODM 托管类都必须有一个可区分的名称作为 ID，所以所有 Spring LDAP 存储库都必须将 ID 类型参数设置为`javax.naming.Name`。内置的`LdapRepository`只接受一个类型参数：托管实体类，默认 ID 为`javax.naming.Name`。

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

Spring LDAP 中包含了基本的 QueryDSL 支持。这种支持包括以下方面：

* 一种称为`LdapAnnotationProcessor`的注释处理器，用于基于 Spring LDAP ODM 注释生成 QueryDSL 类。有关 ODM 注释的更多信息，请参见[[ODM]]。

* 一个名为`QueryDslLdapQuery`的查询实现，用于在代码中构建和运行 QueryDSL 查询。

* Spring 数据存储库支持 QueryDSL 谓词。`QueryDslPredicateExecutor`包括一些具有适当参数的附加方法。你可以与`LdapRepository`一起扩展该接口，以便在存储库中包含此支持。

\== 汇集支持

池 LDAP 连接有助于减少为每个 LDAP 交互创建新的 LDAP 连接的开销。虽然[Java LDAP 池支持](https://java.sun.com/products/jndi/tutorial/ldap/connect/pool.html)存在，但它的配置选项和功能（例如连接验证和池维护）受到限制。 Spring LDAP 在 per-`ContextSource`的基础上提供了对详细池配置的支持。

池支持是通过在应用程序上下文配置中向`<ldap:context-source />`元素提供`<ldap:pooling />`子元素来提供的。只读和读写`DirContext`对象是分开池的（如果指定了`anonymous-read-only`）。[Jakarta Commons-游泳池](https://commons.apache.org/pool/index.html)用于提供底层池实现。

\===`DirContext`验证

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

如果配置了连接验证，则池连接将通过使用`DefaultDirContextValidator`进行验证。`DefaultDirContextValidator`是否使用空名称、`DirContext.search(String, String, SearchControls)`的过滤器和`SearchControls`设置来限制仅带有`objectclass`属性和 500ms 超时的单个结果。如果返回的`NamingEnumeration`有结果，则`DirContext`将通过验证。如果没有返回结果或引发异常，则`DirContext`验证失败。在大多数 LDAP 服务器上，默认设置应该在不更改配置的情况下工作，并且提供了验证`DirContext`的最快方法。如果需要定制，可以通过使用[[pool-configuration]]中描述的验证配置属性来实现。

|   |如果连接抛出一个被认为是非瞬态的异常，则该连接将自动失效。例如，如果一个`DirContext`实例抛出一个`javax.naming.CommunicationException`，它将被解释为一个非瞬时错误，并且该实例将自动失效，而不会产生额外的`testOnReturn`操作的开销。通过使用`PoolingContextSource`的`nonTransientExceptions`属性配置被解释为非瞬态的异常。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

\=== 池配置在`<ldap:pooling />`元素上可用于配置 DirContext 池的以下属性：

|              Attribute               |                          Default                           |说明|
|--------------------------------------|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|             `max-active`             |                            `8`                             |每种类型（只读或读写）可同时从该池分配的活动连接的最大数量。你可以使用非正数，没有限制。|
|             `max-total`              |                            `-1`                            |可以同时从这个池中分配的活动连接的最大总数（对于所有类型）。你可以使用非正数，没有限制。|
|              `max-idle`              |                            `8`                             |每种类型（只读或读写）的活动连接的最大数量，这些连接可以在池中保持空闲状态，而不释放额外的连接。你可以使用非正数，没有限制。|
|              `min-idle`              |                            `0`                             |每种类型（只读或读写）的活动连接的最小数量，这些连接可以在池中保持空闲状态，而不会创建额外的连接。你可以使用 zero（默认值）创建 none。|
|              `max-wait`              |                            `-1`                            |在抛出异常之前，池等待连接返回的最大毫秒数（当没有可用的连接时）。你可以使用非正数无限期地等待。|
|           `when-exhausted`           |                          `BLOCK`                           |指定池耗尽时的行为。<br/><br/>* 当池耗尽时，`FAIL`选项抛出`NoSuchElementException`。<br/><br/>*`BLOCK`选项等待新对象可用。如果`max-wait`为正，并且在`max-wait`时间过期后没有新的对象可用，则抛出`NoSuchElementException`。<br/><br/>*`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 minutes)                |对象在池中闲置的最短时间，直到它有资格被空闲对象驱逐者（如果有的话）驱逐。|
|       `validation-query-base`        |                  `LdapUtils.emptyName()`                   |验证连接时使用的搜索库。仅在指定`test-on-borrow`、`test-on-return`或`test-while-idle`时使用。|
|      `validation-query-filter`       |                      `objectclass=*`                       |验证连接时使用的搜索过滤器。仅在指定`test-on-borrow`、`test-on-return`或`test-while-idle`时使用。|
|`validation-query-search-controls-ref`|`null`; default search control settings are described above.|验证连接时要使用的`SearchControls`实例的 ID。仅在指定`test-on-borrow`、`test-on-return`或`test-while-idle`时使用。|
|      `non-transient-exceptions`      |           `javax.naming.CommunicationException`            |用逗号分隔的`Exception`类的列表。所列出的例外情况被认为是非瞬时的，与急切的无效有关。如果对池`DirContext`实例的调用引发了任何列出的异常（或其子类），则该对象将自动失效，而无需进行任何额外的 TestonReturn 操作。|

\=== 池 2 配置

以下属性在`<ldap:pooling2 />`元素上可用，用于配置`DirContext`池：

|              Attribute               |                          Default                           |说明|
|--------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|             `max-total`              |                            `-1`                            |可以同时从这个池中分配的活动连接的最大总数（对于所有类型）。你可以使用非正数，没有限制。|
|         `max-total-per-key`          |                            `8`                             |每个键对池分配的对象实例（已签出或空闲）数量的限制。当达到极限时，分池就耗尽了。负值表示没有限制。|
|          `max-idle-per-key`          |                            `8`                             |每种类型（只读或读写）在池中可以保持空闲的活动连接的最大数量，而不释放额外的连接。负值表示没有限制。|
|          `min-idle-per-key`          |                            `0`                             |每种类型（只读或读写）的活动连接的最小数量，这些连接可以在池中保持空闲状态，而不会创建额外的连接。你可以使用 zero（默认值）创建 none。|
|              `max-wait`              |                            `-1`                            |在抛出异常之前，池等待一个连接返回的最大毫秒数（当没有可用的连接时）。你可以使用非正数无限期地等待。|
|        `block-when-exhausted`        |                           `true`                           |是否要等到一个新对象可用。如果 max-wait 为正，则如果`maxWait`时间过期后没有可用的新对象，则抛出一个`NoSuchElementException`。|
|           `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 minutes)                |对象在池中闲置的最短时间，直到它有资格被空闲对象驱逐者（如果有的话）驱逐。|
|   `soft-min-evictable-time-millis`   |                            `-1`                            |对象在符合 IdleObjectOvertor 的驱逐条件之前可以在池中空闲的最短时间，附加条件是每个键在池中保留的对象实例的最小数量。如果将此设置设置设置为正值，则将被`min-evictable-time-millis`覆盖。|
|       `eviction-policy-class`        |   `org.apache.commons.pool2.impl.DefaultEvictionPolicy`    |该池使用的驱逐策略实现。池尝试通过使用线程上下文类装入器来加载类。如果失败，池将尝试使用加载该类的类装入器来加载类。|
|              `fairness`              |                          `false`                           |池为那些等待公平地借用连接的线程提供服务。`true`表示等待线程就像在 FIFO 队列中等待一样被服务。|
|             `jmx-enable`             |                           `true`                           |Pool 的平台 MBean 服务器启用了 JMX。|
|           `jmx-name-base`            |                           `null`                           |作为分配给启用 JMX 的池的名称的一部分使用的 JMX 名基。|
|          `jmx-name-prefix`           |                           `pool`                           |作为分配给启用 JMX 的池的名称的一部分而使用的 JMX 名称前缀。|
|                `lifo`                |                           `true`                           |指示器，用于显示池是否具有相对于空闲对象的 LIFO（后进先出）行为，还是作为 FIFO（先进先出）队列。LIFO 总是返回池中最近使用的对象，而 FIFO 总是返回空闲对象池中最老的对象|
|       `validation-query-base`        |                  `LdapUtils.emptyPath()`                   |用于验证搜索的基本 DN。|
|      `validation-query-filter`       |                      `objectclass=*`                       |用于验证查询的筛选器。|
|`validation-query-search-controls-ref`|`null`; default search control settings are described above.|验证连接时要使用的`SearchControls`实例的 ID。仅在指定`test-on-borrow`、`test-on-return`或`test-while-idle`时使用|
|      `non-transient-exceptions`      |           `javax.naming.CommunicationException`            |用逗号分隔的`Exception`类的列表。所列出的例外情况被认为是非瞬时的，与急切的无效有关。如果对池`DirContext`实例的调用引发了任何列出的异常（或其子类），则该对象将自动失效，而无需进行任何额外的 TestonReturn 操作。|

\=== 配置

配置池需要在`<ldap:context-source>`元素中添加嵌套的`<ldap:pooling>`元素，如下所示：

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

在实际情况下，你可能会配置池选项并启用连接验证。前面的例子说明了一般的概念。

\==== 验证配置

下面的示例在将每个`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>
```

\=== 已知问题

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

* [[ Spring-LDAP-known-issues-custom-authentication]（# Spring-LDAP-known-issues-custom-authentication）

\==== 自定义身份验证

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

\== 添加缺失的重载 API 方法

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

\=== 实现自定义搜索方法

`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`的可用实现之一，该实现在内部列表中收集映射的结果。为了实际执行搜索，你需要在`LdapTemplate`中调用`search`方法，该方法将一个执行器和一个处理程序作为参数。最后，你需要归还你的处理程序收集的任何内容。下面的示例展示了如何实现所有这些功能：

```
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();
   }
}
```

如果你更喜欢`ContextMapper`而不是`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 ContextMapperCallbackHandler(new PersonContextMapper());

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

|   |当使用`ContextMapperCallbackHandler`时，必须确保在`SearchControls`实例上调用了`setReturningObjFlag(true)`。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------|

\=== 实现其他自定义上下文方法

以与自定义`search`方法相同的方式，你实际上可以通过使用`ContextExecutor`调用`DirContext`中的任何方法，如下所示：

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

在实现自定义`ContextExecutor`时，可以在使用`executeReadOnly()`或`executeReadWrite()`方法之间进行选择。假设你想调用以下方法：

```
Object lookupLink(Name name)
```

该方法在`DirContext`中可用，但在`LdapTemplate`中没有匹配方法。它是一种查找方法，因此应该是只读的。我们可以通过以下方式来实现这一点：

```
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()`方法执行读写操作。

\== 处理`DirContext`

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

\=== 自定义`DirContext`预处理和后处理

在某些情况下，你可能希望在搜索操作之前和之后对`DirContext`执行操作。用于此目的的接口称为`DirContextProcessor`。下面的清单显示了`DirContextProcessor`接口：

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

`LdapTemplate`类有一个搜索方法，它接受`DirContextProcessor`，如下所示：

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

在搜索操作之前，在给定的`DirContextProcessor`实例上调用`preProcess`方法。在搜索运行并且处理了结果`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)
```

\=== 实现请求控件`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`类似于以下示例：

```
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`]（https://download.oracle.com/javase/1.5.0/DOCS/api/javax/naming/ldap/control.html）接口是针对 LDAPV3 的，并且要求使用`LdapContext`而不是`DirContext`。如果调用一个`AbstractRequestControlDirContextProcessor`子类的参数不是`LdapContext`，则抛出一个`IllegalArgumentException`。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

\=== 分页搜索结果

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

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

Spring LDAP 通过使用用于的前后处理的概念来提供对分页结果的支持，如在前面的部分中讨论的那样。它通过使用`PagedResultsDirContextProcessor`类来实现这一点。`PagedResultsDirContextProcessor`类使用请求的页面大小创建一个`PagedResultsControl`，并将其添加到`LdapContext`中。在搜索之后，它获得`PagedResultsResponseControl`并检索分页结果 cookie，这是在连续的分页结果请求之间保持上下文所需的。

下面的示例展示了如何使用分页搜索结果功能：

```
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`来实现这一点，如前面的示例所示。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

\== 事务支持

曾经在 LDAP 世界中使用关系数据库的程序员经常对没有事务的概念这一事实表示惊讶。协议中没有指定它，也没有 LDAP 服务器支持它。 Spring 意识到这可能是一个主要问题，LDAP 提供了对客户端的支持，对 LDAP 资源上的事务进行补偿。

LDAP 事务支持是由`ContextSourceTransactionManager`、一个`PlatformTransactionManager`实现提供的，该实现管理 Spring 用于 LDAP 操作的事务支持。它与协作者一起跟踪在事务中执行的 LDAP 操作，在每次操作之前记录状态，并在事务需要回滚时采取步骤恢复初始状态。

除了实际的事务管理， Spring LDAP 事务支持还确保在整个相同的事务中使用相同的`DirContext`实例。也就是说，在事务完成之前，`DirContext`实际上不会关闭，从而允许更有效地使用资源。

|   |虽然 Spring LDAP 用于提供事务支持的方法在许多情况下是足够的，但它绝不是传统意义上的“真正的”事务。<br/>服务器完全不知道这些事务，因此（例如），如果连接被中断，没有回滚事务的方法。<br/>虽然应该仔细考虑这一点，但也应该注意，替代方案是在没有任何事务支持的情况下进行操作。 Spring LDAP 的事务支持几乎和它得到的一样好。|
|---| |

|   |客户端事务支持除了原始操作所需的工作之外，还增加了一些开销。<br/>虽然在大多数情况下，这种开销不应该担心，但<br/>如果你的应用程序不在同一个事务中执行多个 LDAP 操作（例如，`modifyAttributes`后接`rebind`），<br/>或者如果不需要与 JDBC 数据源进行事务同步（参见[[ Spring-ldap-jdbc-transaction-integration]]（# Spring-ldap-jdbc-transaction-integration）），则通过使用 LDAP 事务支持几乎不会获得什么好处。|
|---| |

\=== 配置

如果你习惯于配置 Spring 事务，那么配置 Spring LDAP 事务应该看起来非常熟悉。你可以使用`@Transactional`注释你的事务类，创建`TransactionManager`实例，并在 Bean 配置中包含一个`<tx:annotation-driven>`元素。下面的示例展示了如何做到这一点：

```
<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 />
...
```

|   |虽然这种设置对于大多数简单的用例都很好，但一些更复杂的场景需要额外的配置。<br/>具体来说，如果需要在事务中创建或删除子树，则需要使用替代的`TempEntryRenamingStrategy`，如[[renaming-strategies]]（#reaming-strategies）中所述。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在实际情况中，你可能会在服务对象级别而不是存储库级别上应用事务。前面的例子说明了一般的概念。

\===JDBC 事务集成

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

虽然不支持实际的 XA 事务，但通过向`<ldap:transaction-manager>`元素提供`data-source-ref`属性，在概念上支持将 JDBC 和 LDAP 访问打包到同一个事务中。这将创建一个`ContextSourceAndDataSourceTransactionManager`，然后将这两个事务虚拟地管理为一个。在执行提交时，总是首先执行操作的 LDAP 部分，如果 LDAP 提交失败，则允许回滚这两个事务。事务的 JDBC 部分完全按照`DataSourceTransactionManager`中的方式进行管理，但不支持嵌套事务。下面的示例显示了带有`data-source-ref`属性的`ldap:transaction-manager`元素：

```
<ldap:transaction-manager data-source-ref="dataSource" >
  <ldap:default-renaming-strategy />
<ldap:transaction-manager />
```

|   |所提供的支持都是客户端的。<br/>打包的事务不是 XA 事务。不执行两阶段提交，因为 LDAP 服务器不能对其结果进行投票。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

通过向`<ldap:transaction-manager>`元素提供`session-factory-ref`属性，你可以为 Hibernate 集成完成相同的事情，如下所示：

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

\===LDAP 补偿事务解释

Spring LDAP 通过在每次修改操作之前在 LDAP 树中记录状态来管理补偿事务（`bind`，`unbind`，`rebind`，`modifyAttributes`，和`rename`）。这使系统能够在事务需要回滚时执行补偿操作。

在许多情况下，补偿操作非常简单。例如，对`bind`操作的补偿回滚操作是解除条目的绑定。然而，由于 LDAP 数据库的某些特定特性，其他操作需要一种不同的、更复杂的方法。具体地说，并不总是能够获得条目的所有`Attributes`的值，这使得前述策略对于（例如）`unbind`操作是不够的。

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

|  LDAP Operation  |录音|                Preparation                |                                                Commit                                                 |                                                Rollback                                                 |
|------------------|----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|      `bind`      |记录要绑定的条目的 DN。|              Bind the entry.              |                                             No operation.                                             |                               Unbind the entry by using the recorded DN.                                |
|     `rename`     |记录原始数据和目标数据.|             Rename the entry.             |                                             No operation.                                             |                                Rename the entry back to its original DN.                                |
|     `unbind`     |对原始 DN 进行记录，并计算出一个临时 DN。|Rename the entry to the temporary location.|                                      Unbind the temporary entry.                                      |                  Rename the entry from the temporary location back to its original DN.                  |
|     `rebind`     |记录原始 DN 和新的`Attributes`，并计算一个临时 DN。| Rename the entry to a temporary location. |Bind the new `Attributes` at the original DN and unbind the original entry from its temporary location.|                  Rename the entry from the temporary location back to its original DN.                  |
|`modifyAttributes`|对 DN 的条目进行记录，以修改和计算补偿`ModificationItem`实例所要做的修改。| Perform the `modifyAttributes` operation. |                                             No operation.                                             |Perform a `modifyAttributes` operation by using the calculated compensating `ModificationItem` instances.|

Spring LDAP 事务支持的内部工作方式的更详细描述可在[Javadoc](https://docs.spring.io/spring-ldap/docs/current/apidocs/)中获得。

\==== 重命名策略

如上一节的表中所描述的，一些操作的事务管理需要在提交中进行实际修改之前对受操作影响的原始条目进行临时重命名。计算条目的临时 DN 的方式由配置中的`<ldap:transaction-manager >`声明的子元素中指定的`TempEntryRenamingStrategy`管理。 Spring LDAP 包括两种实现方式：

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

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

|   |`DefaultTempEntryRenamingStrategy`在某些情况下不起作用。例如，如果计划进行递归删除，则需要使用`DifferentSubtreeTempEntryRenamingStrategy`。这是因为递归删除操作实际上包括对子树中的每个节点分别进行深度优先删除。由于你不能重命名一个包含任何子项的条目，并且`DefaultTempEntryRenamingStrategy`将使每个节点留在同一个子树中（使用不同的名称），而不是实际删除它，因此此操作将失败。有疑问时，使用`DifferentSubtreeTempEntryRenamingStrategy`。|
|---| |

\== 使用 Spring LDAP 的用户身份验证

本节涵盖了使用 Spring LDAP 的用户身份验证。它包含以下主题：

* [[ Spring-LDAP-User-Authentication-Basic]（# Spring-LDAP-User-Authentication-Basic）

* [[OperationsonAuthenticatedContext]]

* [[ Spring-LDAP-身份验证-过时]（# Spring-LDAP-身份验证-过时）

* [[ Spring-LDAP-using- Spring-security]]（# Spring-LDAP-using- Spring-security）

\=== 基本身份验证

虽然`ContextSource`的核心功能是提供`DirContext`实例供`LdapTemplate`使用，但你也可以使用它针对 LDAP 服务器对用户进行身份验证。`getContext(principal, credentials)`的`ContextSource`方法就是这样做的。它根据`ContextSource`配置构造`DirContext`实例，并使用提供的主体和凭据对上下文进行身份验证。自定义身份验证方法可能看起来像以下示例：

```
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);
  }
}
```

提供给`authenticate`方法的`userDn`需要是要进行身份验证的用户的完整 DN（无论`base`上的`base`设置如何）。通常需要基于（例如）用户名执行 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);`。

使用此方法时，身份验证变得如下所示的简单：

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

|   |正如[下一节](#operationsOnAuthenticatedContext)中所描述的，一些设置可能需要你执行额外的操作才能进行实际的身份验证。有关详细信息，请参见[[OperationsonAuthenticatedContext]]。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |不要编写自己的自定义身份验证方法。使用 Spring LDAP 中提供的那些。|
|---|----------------------------------------------------------------------------------------|

\=== 在经过验证的上下文上执行操作

一些身份验证方案和 LDAP 服务器需要在创建的`DirContext`实例上执行一些操作，才能进行实际的身份验证。你应该测试并确保你的服务器设置和身份验证方案的行为。如果做不到这一点，可能会导致用户被允许进入你的系统，而不考虑所提供的 DN 和凭据。下面的示例展示了一个验证方法的天真实现，其中在经过验证的上下文上执行硬编码的`lookup`操作：

```
public boolean authenticate(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);`

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

```
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);
```

\=== 过时的身份验证方法

除了前面几节中描述的`authenticate`方法外，还可以使用许多不推荐的方法进行身份验证。虽然这些方法很好，但我们建议使用`LdapQuery`方法。

\=== 使用 Spring 安全性

虽然前面几节中描述的方法对于简单的身份验证场景可能已经足够了，但这一领域的需求通常会迅速扩展。应用了许多方面，包括身份验证、授权、Web 集成、用户上下文管理和其他方面。如果你怀疑需求可能不仅仅是简单的身份验证，那么你肯定应该考虑出于安全目的使用[Spring Security](https://spring.io/spring-security)。它是一个功能齐全、成熟的安全框架，能够解决上述几个方面以及其他几个方面的问题。

\==LDIF 解析

LDAP 目录交换格式文件是以平面文件格式描述目录数据的标准媒介。这种格式最常见的用途包括信息传输和存档。然而，该标准还定义了一种以平面文件格式描述对存储数据的修改的方法。这种后类型的 LDIF 通常称为*变化类型*或*修改*LDIF。

`org.springframework.ldap.ldif`包提供了解析 LDIF 文件并将它们反序列化为有形对象所需的类。`LdifParser`是`org.springframework.ldap.ldif`包的主要类，并且能够解析符合 RFC2849 的文件。这个类从资源中读取行，并将它们组装到`LdapAttributes`对象中。

|   |`LdifParser`当前忽略*变化类型*LDIF 条目，因为它们在应用程序上下文中的有用性尚未确定。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------|

\=== 对象表示

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

* `LdapAttribute`:扩展`javax.naming.directory.BasicAttribute`，增加对 RFC2849 中定义的 LDIF 选项的支持。

* `LdapAttributes`:扩展`javax.naming.directory.BasicAttributes`添加对 DNS 的专门支持。

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

\=== 解析器

`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 规范允许属性跨多行。默认策略根据阅读行的顺序来评估行，以确定所考虑行的性质。*控制*属性和*变化类型*记录被忽略。

`DefaultAttributeValidationPolicy`使用正则表达式来确保每个属性在解析后都符合有效的属性格式（根据 RFC2849）。如果属性验证失败，则会记录`InvalidAttributeFormatException`，并跳过记录（解析器返回`null`）。

\=== 模式验证

通过`org.springframework.ldap.schema`包中的`Specification`接口，可以使用一种针对模式验证解析对象的机制。`DefaultSchemaSpecification`不执行任何验证，可用于已知记录有效且无需检查的实例。此选项节省了验证所带来的性能损失。`BasicSchemaSpecification`应用基本检查，例如确保提供了 DN 和对象类声明。目前，针对实际模式的验证需要实现`Specification`接口。

\=== Spring 批处理集成

Spring 虽然`LdifParser`可以被任何需要解析 LDIF 文件的应用程序所采用，但它提供了一种批处理框架，该框架提供了许多用于解析分隔文件的文件的文件处理实用程序，例如 CSV。`org.springframework.ldap.ldif.batch`包提供了在 Spring 批处理框架中使用`LdifParser`作为有效配置选项所需的类。这套教材有五门课。它们一起提供了三个基本的用例：

* 从文件中读取 LDIF 记录并返回`LdapAttributes`对象。

* 从文件中读取 LDIF 记录，并将记录映射到 Java 对象。

* 将 LDIF 记录写入文件。

第一个用例是用`LdifReader`完成的。这个类扩展 Spring 批的`AbstractItemCountingItemStreamItemReader`并实现其`ResourceAwareItemReaderItemStream`。它自然地适合于框架，你可以使用它从文件中读取`LdapAttributes`对象。

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

你可以实现`RecordCallbackHandler`并将实现提供给任一读取器。你可以使用此处理程序对跳过的记录进行操作。有关更多信息，请参见[Spring Batch API documentation](https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/ldif/RecordCallbackHandler.html)。

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

\== 实用程序

本节描述了可以与 Spring LDAP 一起使用的其他实用程序。

\=== 多值属性的增量检索

当特定属性有大量属性值（\>1500）时，Active Directory 通常拒绝一次返回所有这些值。而是根据[多值属性的增量检索](https://tools.ietf.org/html/draft-kashi-incremental-00)方法返回属性值。这样做需要调用部分检查返回的属性中的特定标记，并在必要时发出额外的查找请求，直到找到所有的值。

Spring LDAP 的`org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper`在处理这种属性时有帮助，如下所示：

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

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

\== 测试

这一部分包括用 Spring LDAP 进行测试。它包含以下主题：

* [[ Spring-LDAP-testing-embedded-server]]（# Spring-LDAP-testing-embedded-server）

* [[ Spring-LDAP-testing-apacheds]]（# Spring-LDAP-testing-apacheds）

* [[ Spring-ldap-testing-unboundid]]（# Spring-ldap-testing-unboundid）

\=== 使用嵌入式服务器

`spring-ldap-test`提供了一个基于[ApacheDS](https://directory.apache.org/apacheds/)或[UnboundID](https://www.ldap.com/unboundid-ldap-sdk-for-java)的嵌入式 LDAP 服务器。

|   |`spring-ldap-test`与 ApacheDS1.5.5 兼容。不支持更新版本的 ApacheDS。|
|---|---------------------------------------------------------------------------------------------------|

要开始，你需要包括`spring-ldap-test`依赖项。

下面的清单显示了如何包含 Maven 的`spring-ldap-test`:

```
<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-test</artifactId>
    <version>2.3.5.RELEASE</version>
    <scope>test</scope>
</dependency>
```

下面的清单显示了如何包含 Gradle 的`spring-ldap-test`:

```
testCompile "org.springframework.ldap:spring-ldap-test:2.3.5.RELEASE"
```

\=== 阿帕切德

要使用 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`填充 LDAP 服务器的机制。要使用它，创建一个类似于以下内容的 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 服务器的方法。

\===unboundid

要使用 unboundid，你需要包含一个 unboundid 依赖项。

下面的示例展示了如何为 Maven 包含无边界依赖关系：

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

下面的示例展示了如何为 Gradle 包含无边界依赖关系：

```
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`填充 LDAP 服务器的方法。要使用它，创建一个类似于以下内容的 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>
```