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 LDAPAttributesMapper
和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
search 方法确保创建了一个实例DirContext
,执行搜索,使用给定的将属性映射到字符串,AttributesMapper
将字符串收集到内部列表中,最后返回列表。它还确保NamingEnumeration
andDirContext
正确关闭并处理可能发生的任何异常。
当然,这是一个 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 的亮点如下:
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
和相关的类已被弃用,取而代之的是标准 JavaLdapName
。有关库在处理对象时如何提供帮助的信息,请参阅动态构建可分辨名称。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。致谢
感谢Structure101提供的开源许可证,它可以方便地检查项目结构。
3.基本用法
本节介绍使用 Spring LDAP 的基础知识。它包含以下内容:
3.1。搜索和查找使用AttributesMapper
以下示例使用 anAttributesMapper
来构建所有人员对象的所有常用名称的列表。
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
对象,如下所示:
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
对象的查找:
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
,将返回的属性限制为cn
和sn
,过滤器为(&(objectclass=person)(sn=?))
,我们希望?
将 替换为lastName
参数的值。以下示例显示了如何使用LdapQueryBuilder
:
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 动态构建或修改专有名称的 API
LdapName
很麻烦。提取索引或(特别是)命名组件的值也有点尴尬。 -
许多关于
LdapName
抛出检查异常的操作,需要try-catch
针对错误通常是致命的并且无法以有意义的方式修复的情况的语句。
为了简化使用专有名称,Spring LDAP 提供了LdapNameBuilder
一个.LdapUtils
LdapName
3.3.1。例子
本节介绍了前面几节中涵盖的主题的几个示例。第一个示例LdapName
通过使用动态构建LdapNameBuilder
:
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
具有以下属性:
属性名称 | 属性值 |
---|---|
|
瑞典 |
|
某公司 |
|
一些人 |
然后,前面的代码将产生以下可分辨名称:
cn=Some Person, ou=Some Company, c=Sweden, dc=example, dc=com
以下示例使用以下示例从专有名称中提取值LdapUtils
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
:
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
:
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
:
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
. 此操作采用一组显式属性修改并在特定条目上执行它们,如下所示:
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
数组是很多工作。然而,正如我们在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
来转换找到的值,如下所示:
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
andAttribute
类。这在使用多值属性时特别有用。从多值属性中提取值通常需要遍历NamingEnumeration
从实现返回的属性值Attributes
。
DirContextAdapter
在getStringAttributes()
orgetObjectAttributes()
方法中为您执行此操作。以下示例使用该getStringAttributes
方法:
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
. 使用AbstractContextMapper
,PersonContextMapper
前面所示的可以重写如下:
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
中介绍的存储库方法的改进实现:
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()
时使用的方法。objectclass
该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);
}
}
当没有映射器传递给 aldapTemplate.lookup()
时,结果是一个DirContextAdapter
实例。当lookup
方法返回 anObject
时,lookupContext
便捷方法方法自动将返回值转换为 a DirContextOperations
(DirContextAdapter
实现的接口)。
请注意,我们在create
andupdate
方法中有重复的代码。此代码从域对象映射到上下文。可以提取到单独的方法中,如下:
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"))
,它不会呈现修改,如下例所示:
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
,以下示例显示了Person
LDAP 的完整 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) 是通过使用对象的属性来构造的。在前面的示例中,Person DN 中使用了国家、公司和全名,这意味着更新这些属性中的任何一个实际上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" })
. 但是,如果要管理定义在inetOrgPerson
objectclass 中的属性,则需要使用以下内容:@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
可以使用如下的对象映射方法:
@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
属性值,使组修改变得容易,如以下示例所示:
@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()
,属性修改是通过使用专有名称相等来计算的,这意味着在确定它们是否相等时将忽略专有名称的文本格式。
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
:
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
:
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
:
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
:
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
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、用户名和密码,如下所示:
<ldap:context-source
username="cn=Administrator"
password="secret"
url="ldap://localhost:389" />
前面的示例LdapContextSource
使用默认值(请参阅本段后面的表格)以及指定的 URL 和身份验证凭据创建一个。context-source 上的可配置属性如下(必填属性标有*):
属性 | 默认 | 描述 |
---|---|---|
|
|
创建的 bean 的 ID。 |
|
使用 LDAP 服务器进行身份验证时使用的用户名(主体)。这通常是管理员用户的可分辨名称(例如, |
|
|
使用 LDAP 服务器进行身份验证时使用的密码(凭据)。 |
|
|
要使用的 LDAP 服务器的 URL。URL 应采用以下格式: |
|
|
|
基本 DN。配置此属性后,向 LDAP 操作提供和从 LDAP 操作接收的所有专有名称都与指定的 LDAP 路径相关。这可以显着简化针对 LDAP 树的工作。但是,有时您需要访问基本路径。有关这方面的更多信息,请参阅获取对基本 LDAP 路径的引用 |
|
|
定义是否使用匿名(未经身份验证)上下文执行只读操作。请注意, |
|
|
定义用于处理推荐的策略,如此处所述。有效值为:
|
|
|
指定是否应使用本机 Java LDAP 连接池。考虑改用 Spring LDAP 连接池。有关更多信息,请参阅池支持。 |
|
一个 |
|
|
一个 |
|
|
对自定义环境属性的引用,该 |
7.1.1. DirContext
验证
当DirContext
创建实例用于在 LDAP 服务器上执行操作时,通常需要对这些上下文进行身份验证。Spring LDAP 提供了各种配置选项。
本节涉及在 的核心功能中对上下文进行身份验证ContextSource ,以构造DirContext 供LdapTemplate . LDAP 通常仅用于用户身份验证,ContextSource 也可以用于此目的。该过程在使用 Spring LDAP 的用户身份验证中进行了讨论。
|
默认情况下,为只读和读写操作创建经过身份验证的上下文。您应该指定要用于对元素进行身份验证的 LDAP 用户的username
和。password
context-source
如果是 LDAP 用户的专有名称 (DN),则无论是否在元素
上指定了 LDAP 路径username ,它都必须是从 LDAP 树根开始的用户的完整 DN 。base context-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 服务器提供了两种不同的配置选项:DefaultTlsDirContextAuthenticationStrategy
和ExternalTlsDirContextAuthenticationStrategy
. 两种实现都在目标连接上协商 TLS 通道,但它们的实际身份验证机制不同。在DefaultTlsDirContextAuthenticationStrategy
安全通道上应用 SIMPLE 身份验证(通过使用指定的username
and 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-source
authentication-source-ref
username
password
AuthenticationSource
ContextSource
Context
如果您使用Spring Security ,您可以通过配置Spring Security 附带ContextSource
的实例来确保始终使用当前登录用户的主体和凭据。SpringSecurityAuthenticationSource
以下示例显示了如何执行此操作:
<beans>
...
<ldap:context-source
url="ldap://localhost:389"
authentication-source-ref="springSecurityAuthenticationSource"/>
<bean id="springSecurityAuthenticationSource"
class="org.springframework.security.ldap.authentication.SpringSecurityAuthenticationSource" />
...
</beans>
我们没有指定任何username 或password 为。我们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.2. LdapTemplate
配置
LdapTemplate
是通过使用<ldap:ldap-template>
元素来定义的。最简单的ldap-template
声明是元素本身:
<ldap:ldap-template />
元素本身创建一个LdapTemplate
具有默认 ID 的实例,引用 default ContextSource
,它的 ID 应为(元素contextSource
的默认值)。context-source
下表描述了 上的可配置属性ldap-template
:
属性 | 默认 | 描述 |
---|---|---|
|
|
创建的 bean 的 ID。 |
|
|
|
|
|
搜索的默认计数限制。0 表示没有限制。 |
|
|
搜索的默认时间限制,以毫秒为单位。0 表示没有限制。 |
|
|
搜索的默认搜索范围。有效值为:
|
|
|
指定是否 |
|
|
指定是否 |
|
|
7.3. 获取对基本 LDAP 路径的引用
如前所述,您可以为 提供基本 LDAP 路径ContextSource
,指定 LDAP 树中所有操作都与之相关的根。这意味着您在整个系统中只使用相对可分辨的名称,这通常非常方便。但是,在某些情况下,您可能需要访问基本路径才能构建相对于 LDAP 树的实际根的完整 DN。一个例子是使用 LDAP 组(例如,groupOfNames
对象类)。在这种情况下,每个组成员属性值都需要是被引用成员的完整 DN。
出于这个原因,Spring LDAP 具有一种机制,通过该机制,任何 Spring 控制的 bean 都可以在启动时提供基本路径。对于要通知基本路径的 bean,需要做好两件事。首先,想要基本路径引用的 bean 需要实现BaseLdapNameAware
接口。其次,您需要BaseLdapPathBeanPostProcessor
在应用程序上下文中定义一个。下面的例子展示了如何实现BaseLdapNameAware
:
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
:
<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
使用. 如果定义了多个,则需要通过设置属性来指定使用哪一个。BaseLdapPathSource
AbstractContextSource
ApplicationContext
BaseLdapPathSource
baseLdapPathSourceName
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 池的元素:
属性 | 默认 | 描述 |
---|---|---|
|
|
可以同时从此池中分配的每种类型(只读或读写)的最大活动连接数。您可以无限制地使用非正数。 |
|
|
可以同时从此池中分配的活动连接的最大总数(适用于所有类型)。您可以无限制地使用非正数。 |
|
|
在不释放额外连接的情况下,可以在池中保持空闲状态的每种类型(只读或读写)的最大活动连接数。您可以无限制地使用非正数。 |
|
|
可以在池中保持空闲而不创建额外连接的每种类型(只读或读写)的最小活动连接数。您可以使用零(默认值)来创建无。 |
|
|
池等待(当没有可用连接时)在抛出异常之前返回连接的最大毫秒数。您可以使用非正数无限期等待。 |
|
|
指定池耗尽时的行为。
|
|
|
对象在从池中借用之前是否经过验证。如果对象验证失败,则将其从池中删除,并尝试借用另一个对象。 |
|
|
对象在返回池之前是否经过验证。 |
|
|
对象是否由空闲对象驱逐器验证(如果有)。如果一个对象验证失败,它就会从池中删除。 |
|
|
空闲对象驱逐线程运行之间休眠的毫秒数。当非正数时,不运行空闲对象驱逐线程。 |
|
|
每次运行空闲对象驱逐线程(如果有)期间要检查的对象数。 |
|
|
一个对象在被空闲对象驱逐者(如果有的话)驱逐之前可以在池中闲置的最短时间。 |
|
|
验证连接时要使用的搜索库。仅在指定 |
|
|
验证连接时要使用的搜索过滤器。仅在指定 |
|
|
|
|
|
逗号分隔的 |
9.3. 池 2 配置
以下属性可用于配置池的<ldap:pooling2 />
元素:DirContext
属性 | 默认 | 描述 |
---|---|---|
|
|
可以同时从此池中分配的活动连接的最大总数(适用于所有类型)。您可以无限制地使用非正数。 |
|
|
每个键的池分配的对象实例数的限制(签出或空闲)。当达到限制时,子池被耗尽。负值表示没有限制。 |
|
|
可以在池中保持空闲状态而不会释放额外连接的每种类型(只读或读写)的最大活动连接数。负值表示没有限制。 |
|
|
可以在池中保持空闲且不创建额外连接的每种类型(只读或读写)的最小活动连接数。您可以使用零(默认值)来创建无。 |
|
|
池等待(当没有可用连接时)在抛出异常之前返回连接的最大毫秒数。您可以使用非正数无限期等待。 |
|
|
是否等到有新对象可用。如果 max-wait 为正,则如果在时间到期 |
|
|
借用前是否验证对象。如果对象验证失败,则借用失败。 |
|
|
对象在从池中借用之前是否经过验证的指示符。如果对象验证失败,则将其从池中删除,并尝试借用另一个对象。 |
|
|
对象在返回池之前是否经过验证的指示符。 |
|
|
对象是否由空闲对象驱逐器(如果有)验证的指示符。如果一个对象验证失败,它就会从池中删除。 |
|
|
空闲对象驱逐线程运行之间休眠的毫秒数。当非正数时,不运行空闲对象驱逐线程。 |
|
|
每次运行空闲对象驱逐线程(如果有)期间要检查的对象数。 |
|
|
一个对象在被空闲对象驱逐者(如果有的话)驱逐之前可以在池中闲置的最短时间。 |
|
|
一个对象在它有资格被空闲对象驱逐者驱逐之前可以在池中闲置的最短时间,附加条件是每个键至少有最小数量的对象实例保留在池中。 |
|
|
此池使用的逐出策略实现。池尝试使用线程上下文类加载器加载类。如果失败,池会尝试使用加载该类的类加载器来加载该类。 |
|
|
该池为等待公平借用连接的线程提供服务。 |
|
|
池的平台 MBean 服务器启用了 JMX。 |
|
|
JMX 名称库,用作分配给启用 JMX 的池的名称的一部分。 |
|
|
JMX 名称前缀,用作分配给启用 JMX 的池的名称的一部分。 |
|
|
池是否具有相对于空闲对象或作为 FIFO(先进先出)队列的 LIFO(后进先出)行为的指示器。LIFO 总是返回池中最近使用的对象,而 FIFO 总是返回空闲对象池中最旧的对象 |
|
|
用于验证搜索的基本 DN。 |
|
|
用于验证查询的过滤器。 |
|
|
|
|
|
逗号分隔的 |
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.5。已知的问题
本节描述人们使用 Spring LDAP 时有时会出现的问题。目前,它涵盖了以下问题:
9.5.1。自定义身份验证
PoolingContextSource
假设DirContext
从中检索到的所有对象都ContextSource.getReadOnlyContext()
具有相同的环境,并且同样地,DirContext
从其中检索到的所有对象ContextSource.getReadWriteContext()
都具有相同的环境。这意味着用 a 包装LdapContextSource
配置的AuthenticationSource
aPoolingContextSource
不能按预期工作。将使用第一个用户的凭据填充池,并且除非需要新连接,否则不会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
最后,您需要返回处理程序收集的任何内容。以下示例显示了如何执行所有这些操作:
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();
}
}
如果您更喜欢ContextMapper
,AttributesMapper
以下示例显示了它的外观:
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
。它是一种查找方法,因此它应该是只读的。我们可以按如下方式实现:
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
的类似于下面的例子:
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,这是保持连续分页结果请求之间的上下文所必需的。
以下示例显示了如何使用分页搜索结果功能:
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 补偿事务解释
bind
Spring LDAP 通过在每次修改操作( 、unbind
、rebind
、modifyAttributes
和rename
)之前在 LDAP 树中记录状态来管理补偿事务。这让系统在事务需要回滚时执行补偿操作。
在许多情况下,补偿操作非常简单。例如,一个操作的补偿回滚操作bind
是取消绑定条目。然而,由于 LDAP 数据库的某些特殊特性,其他操作需要不同的、更复杂的方法。具体来说,并非总是可以获得所有Attributes
条目的值,使得上述策略不足以(例如)unbind
操作。
这就是为什么在 Spring LDAP 托管事务中执行的每个修改操作在内部都分为四个不同的操作:记录操作、准备操作、提交操作和回滚操作。下表描述了每个 LDAP 操作:
LDAP 操作 | 记录 | 准备 | 犯罪 | 回滚 |
---|---|---|---|---|
|
记录要绑定的条目的DN。 |
绑定条目。 |
无操作。 |
使用记录的 DN 解绑该条目。 |
|
记录原始和目标 DN。 |
重命名条目。 |
无操作。 |
将条目重命名回其原始 DN。 |
|
记录原始DN并计算临时DN。 |
将条目重命名为临时位置。 |
解绑临时条目。 |
将临时位置的条目重命名回其原始 DN。 |
|
记录原始DN和新DN |
将条目重命名为临时位置。 |
在原始 DN 处绑定新 |
将临时位置的条目重命名回其原始 DN。 |
|
记录要修改的条目的 DN,并计算 |
执行 |
无操作。 |
|
Javadoc中提供了有关 Spring LDAP 事务支持的内部工作的更详细描述。
12.3.1. 重命名策略
如上节表格所述,某些操作的事务管理需要将受操作影响的原始条目暂时重命名,然后才能在提交中进行实际修改。计算条目的临时 DN 的方式由配置中声明TempEntryRenamingStrategy
的子元素中指定的 a管理。<ldap:transaction-manager >
Spring LDAP 包括两个实现:
-
DefaultTempEntryRenamingStrategy
(默认):使用<ldap:default-renaming-strategy />
元素指定。将后缀添加到条目 DN 的最不重要部分。例如,对于 DNcn=john doe, ou=users
,此策略返回 DN 的临时 DNcn=john doe_temp, ou=users
。您可以通过设置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 .
|
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
提供给方法的authenticate
DN 必须是用户的完整 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);
.
当您使用此方法时,身份验证变得非常简单,如下所示:
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);
此方法允许对经过身份验证的上下文执行任何操作,如下所示:
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.4. 使用 Spring Security
虽然前几节中描述的方法可能足以满足简单的身份验证场景,但该领域的需求通常会迅速扩展。许多方面都适用,包括身份验证、授权、Web 集成、用户上下文管理等。如果您怀疑要求可能超出简单的身份验证,您绝对应该考虑使用Spring Security来代替您的安全目的。它是一个功能齐全、成熟的安全框架,可以解决上述方面以及其他几个方面的问题。
14. LDIF 解析
LDAP 目录交换格式 (LDIF) 文件是用于以平面文件格式描述目录数据的标准介质。这种格式最常见的用途包括信息传输和存档。但是,该标准还定义了一种以平面文件格式描述对存储数据的修改的方法。后一种类型的 LDIF 通常称为changetype或modify LDIF。
该org.springframework.ldap.ldif
包提供了解析 LDIF 文件并将它们反序列化为有形对象所需的类。这LdifParser
是org.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 规范允许属性跨越多行。默认策略会根据阅读顺序对行进行评估,以确定所考虑行的性质。控制属性和更改类型记录被忽略。
使用DefaultAttributeValidationPolicy
REGEX 表达式来确保每个属性在解析后符合有效的属性格式(根据 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
包提供了使用LdifParser
Spring 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 与 ApacheDS 1.5.5 兼容。不支持较新版本的 ApacheDS。
|
要开始,您需要包含spring-ldap-test
依赖项。
以下清单显示了如何包含spring-ldap-test
Maven:
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-test</artifactId>
<version>{project-version}</version>
<scope>test</scope>
</dependency>
以下清单显示了如何包含spring-ldap-test
for 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 服务器的方法。