Spring LDAP 使得构建使用 Lightweight Directory Access 协议的基于 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的相对快速的介绍。它包括以下内容:
- 概述
- 传统 Java LDAP 与 LdapTemplate
- 2.2 中的新功能
- 2.1 中的新功能
- 2.0 中的新增功能
- 包装概述
- 开始
- 支持
- 确认
2.1. 概述
Spring LDAP旨在简化Java中的LDAP编程。该库提供的一些功能包括:
- Jdbc模板样式的模板简化为LDAP编程。
- JPA 或休眠样式的基于注释的对象和目录映射。
- 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和类,我们获得了与以下代码完全相同的功能:AttributesMapperLdapTemplate
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(); } }); }}样板代码的数量明显少于传统示例。 搜索方法确保创建实例,执行搜索,使用给定的 , 将属性映射到字符串 收集内部列表中的字符串,最后返回列表。它还确保 和 正确关闭和 处理可能发生的任何异常。LdapTemplateDirContextAttributesMapperNamingEnumerationDirContext
当然,这是一个 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 的更新日志。 Spring LDAP 2.2 的亮点如下:
- #415:添加了对 Spring 5 的支持
- #399: 嵌入式未绑定 LDAP 服务器支持
- #410: 添加了共享资源池 2 支持的文档
2.4. 2.1 中的新功能
有关 2.1 的完整详细信息,请参阅 2.1.0.RC1 和 2.1.0 的更新日志 Spring LDAP 2.1 的亮点如下。
- #390:添加了弹簧数据漏斗支持
- #351: 添加了对共享资源池2的支持
- #370:在 XML 命名空间中添加了支持属性占位符
- #392: 添加了文档测试支持
- #401:添加了断言开关
- 从 JIRA 迁移到 GitHub 问题
- 添加了 Gitter 聊天
2.5. 2.0 中的新功能
虽然在 2.0 版中对 Spring LDAP API 进行了相当重要的现代化改造,但已非常小心地确保尽可能向后兼容。 与Spring LDAP 1.3.x一起使用的代码,除了少数例外,应该在使用2.0库时编译并运行,而无需进行任何修改。
例外是少数类已移动到新包中,以便使一些重要的重构成为可能。 移动的类通常不是预期的公共 API 的一部分,迁移过程应该是平稳的。每当升级后找不到 Spring LDAP 类时,都应在 IDE 中组织导入。
不过,您应该会遇到一些弃用警告,并且还有许多其他 API 改进。 尽可能多地从 2.0 版本中获取的建议是摆脱已弃用的类和方法,并迁移到新的、改进的 API 实用程序。
以下列表简要描述了Spring LDAP 2.0中最重要的变化:
- Spring LDAP 现在需要 Java 6。仍然支持从 2.0 及更高版本开始的春季版本。
- 中央 API 已更新为 Java 5+ 功能,如泛型和变量。 因此,整个模块已被弃用,我们鼓励您迁移到使用核心 Spring LDAP 类。 核心接口的参数化会导致现有代码出现大量编译警告,我们鼓励您采取适当的措施来消除这些警告。spring-ldap-tiger
- ODM(对象目录映射)功能已移至核心,并且有新的方法使用这种自动转换来回 ODM 注释类。有关详细信息,请参阅对象目录映射 (ODM)。LdapOperationsLdapTemplate
- 现在(终于)提供了一个自定义XML命名空间来简化Spring LDAP的配置。有关详细信息,请参阅配置。
- Spring LDAP 现在支持 Spring Data Repository 和 QueryDSL。有关更多信息,请参阅 Spring LDAP 存储库。
- Name现在,在 ODM 中可分辨名称相等性方面,可以正确处理作为属性值的实例。 有关详细信息,请参阅 DirContextAdapter 和可分辨名称作为属性值以及 ODM 和可分辨名称作为属性值。DirContextAdapter
- DistinguishedName和关联的类已被弃用,取而代之的是标准 Java。 有关库在处理对象时如何提供帮助的信息,请参阅动态构建可分辨名称。LdapNameLdapName
- 添加了流畅的 LDAP 查询构建支持。这使得在Spring LDAP中使用LDAP搜索时可以获得更愉快的编程体验。 有关 LDAP 查询构建器支持的更多信息,请参阅构建 LDAP 查询和高级 LDAP 查询。
- 中的旧方法已被弃用,取而代之的是一些新方法,这些方法使用对象并在身份验证失败时引发异常,使用户更容易找出导致身份验证尝试失败的原因。authenticateLdapTemplateauthenticateLdapQuery
- 这些示例已经过完善和更新,以利用 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 应用程序上下文连接起来的,则需要。 添加应用程序对象使用一致的 API 获取资源的功能。如果您打算使用 .spring-contextBaseLdapPathBeanPostProcessor
- spring-tx:如果计划使用客户端补偿事务支持,则需要。
- spring-jdbc:如果计划使用客户端补偿事务支持,则需要。
- commons-pool:如果您计划使用池化功能,则需要。
- spring-batch:如果您计划将 LDIF 解析功能与 Spring 批处理一起使用,则需要。
spring-data-ldap传递添加 ,它使用。 正因为如此,Spring LDAP的XML配置支持需要依赖关系,即使Spring Data的功能集没有被使用。spring-repository.xsdspring-ldap.xsd
2.7. 入门
这些示例提供了一些有用的示例,说明如何在常见用例中使用Spring LDAP。
2.8. 支持
如果您有任何疑问,请在带有 spring-ldap 标签的 Stack Overflow 上提问。 项目网页 https://spring.io/spring-ldap/。
2.9. 致谢
启动Spring LDAP项目的最初努力是由Jayway赞助的。 该项目目前的维护由Pivotal资助,该公司已被VMware收购。
感谢 Structure101 提供了一个开源许可证,该许可证在检查项目结构方面派上用场。
3. 基本用法
本节介绍使用 Spring LDAP 的基础知识。它包含以下内容:
- 使用 AttributesMapper 进行搜索和查找
- 构建 LDAP 查询
- 动态构建可分辨名称
- 例子
- 绑定和解绑
- 更新
3.1. 搜索和查找使用AttributesMapper
下面的示例使用 AttributesMapper 生成所有 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(); } }); }}的内联实现从对象获取所需的属性值并返回它。在内部,循环访问找到的所有条目,为每个条目调用给定的条目,并在列表中收集结果。然后,该方法返回该列表。AttributesMapperAttributesLdapTemplateAttributesMappersearch
请注意,可以轻松修改实现以返回完整对象,如下所示:AttributesMapperPerson
例 2.返回 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
例 3.生成 Person 对象的查找
package com.example.repo;public class PersonRepoImpl implements PersonRepo { private LdapTemplate ldapTemplate; ... public Person findPerson(String dn) { return ldapTemplate.lookup(dn, new PersonAttributesMapper()); }}前面的示例查找指定的 DN 并将找到的属性传递给提供的属性 — 在本例中,生成一个对象。AttributesMapperPerson
3.2. 构建 LDAP 查询
LDAP 搜索涉及许多参数,包括:
- 基本 LDAP 路径:应在 LDAP 树中的哪个位置开始搜索。
- 搜索范围:搜索应在 LDAP 树中深入多深。
- 要返回的属性。
- 搜索筛选器:选择范围内元素时要使用的条件。
Spring LDAP 提供了一个 LdapQueryBuilder,其中包含一个流畅的 API,用于构建 LDAP 查询。
假设您要从基本 DN 开始执行搜索, 将返回的属性限制为 和 ,并使用过滤器 ,我们希望将 替换为参数的值。 下面的示例演示如何使用 :dc=261consulting,dc=comcnsn(&(objectclass=person)(sn=?))?lastNameLdapQueryBuilder
例 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(); } }); }}除了简化复杂搜索参数的构建外,该类及其关联的类还提供对搜索筛选器中任何不安全字符的正确转义。这可以防止“LDAP 注入”,即用户可能会使用此类字符将不需要的操作注入到 LDAP 操作中。LdapQueryBuilder
LdapTemplate包括许多用于执行 LDAP 搜索的重载方法。这是为了适应尽可能多的不同用例和编程风格偏好。对于绝大多数用例,将 作为输入的方法都是推荐使用的方法。LdapQuery
这只是处理搜索和查找数据时可以使用的可用回调接口之一。有关替代方法,请参阅使用 DirContextAdapter 简化属性访问和操作。AttributesMapper
有关 的详细信息,请参阅高级 LDAP 查询。LdapQueryBuilder
3.3. 动态构建可分辨名称
可分辨名称(LdapName)的标准Java实现 在解析可分辨名称时表现良好。但是,在实际使用中,这种实现存在许多缺点:
- 实现是可变的,这不适合表示标识的对象。LdapName
- 尽管具有可变性,但使用 动态生成或修改可分辨名称的 API 非常繁琐。 提取索引或(特别是)命名组件的值也有点尴尬。LdapName
- 抛出的许多操作都检查了异常,需要针对错误通常是致命且无法以有意义的方式修复的情况的语句。LdapNametry-catch
为了简化使用可分辨名称,Spring LDAP 提供了一个 LdapNameBuilder, 以及 LdapUtils 中的许多实用程序方法,这些方法在使用 .LdapName
3.3.1. 例子
本节介绍前几节中涵盖的主题的几个示例。 第一个示例使用以下方法动态构建:LdapNameLdapNameBuilder
Example 5. Dynamically building an by using LdapNameLdapNameBuilder
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(); } ...}假设 a 具有以下属性:Person
属性名称
属性值
country
瑞典
company
某公司
fullname
某个人
然后,上述代码将生成以下可分辨名称:
cn=Some Person, ou=Some Company, c=Sweden, dc=example, dc=com下面的示例使用LdapUtils
例 6.使用 从可分辨名称中提取值LdapUtils
package com.example.repo;import org.springframework.ldap.support.LdapNameBuilder;import javax.naming.Name;public class PersonRepoImpl implements PersonRepo {... protected Person buildPerson(Name dn, Attributes attrs) { Person person = new Person(); person.setCountry(LdapUtils.getStringValue(dn, "c")); person.setCompany(LdapUtils.getStringValue(dn, "ou")); person.setFullname(LdapUtils.getStringValue(dn, "cn")); // Populate rest of person object using attributes. return person; }}由于 1.4 之前(包括 1.4)的 Java 版本根本没有提供任何公共的可分辨名称实现,Spring LDAP 1.x 提供了自己的实现。 此实现本身存在一些缺点,并在 2.0 版中已弃用。您现在应该与前面描述的实用程序一起使用。DistinguishedNameLdapName
3.4. 绑定和解绑
本节介绍如何添加和删除数据。下一节将介绍更新。
3.4.1. 添加数据
在 Java LDAP 中插入数据称为绑定。这有点令人困惑,因为在LDAP术语中,“绑定”意味着完全不同的东西。 JNDI 绑定执行 LDAP 添加操作,将具有指定可分辨名称的新条目与一组属性相关联。 以下示例使用以下方法添加数据:LdapTemplate
例 7.使用属性添加数据
package com.example.repo;public class PersonRepoImpl implements PersonRepo { private LdapTemplate ldapTemplate; ... public void create(Person p) { Name dn = buildDn(p); ldapTemplate.bind(dn, null, buildAttributes(p)); } private Attributes buildAttributes(Person p) { Attributes attrs = new BasicAttributes(); BasicAttribute ocattr = new BasicAttribute("objectclass"); ocattr.add("top"); ocattr.add("person"); attrs.put(ocattr); attrs.put("cn", "Some Person"); attrs.put("sn", "Person"); return attrs; }}手动属性构建虽然枯燥而冗长,但足以满足许多目的。但是,您可以进一步简化绑定操作,如使用 DirContextAdapter 简化属性访问和操作中所述。
3.4.2. 删除数据
在 Java LDAP 中删除数据称为取消绑定。 JNDI 取消绑定执行 LDAP 删除操作,从 LDAP 树中除去与指定可分辨名称关联的条目。 以下示例使用 删除数据:LdapTemplate
例 8.删除数据
package com.example.repo;public class PersonRepoImpl implements PersonRepo { private LdapTemplate ldapTemplate; ... public void delete(Person p) { Name dn = buildDn(p); ldapTemplate.unbind(dn); }}3.5. 更新
在 Java LDAP 中,可以通过两种方式修改数据:使用或使用 .rebindmodifyAttributes
3.5.1. 使用重新绑定进行更新
A 是修改数据的粗略方法。它基本上是一个后跟一个. 以下示例使用:rebindunbindbindrebind
例 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.使用修改属性进行修改
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}); }}构建和阵列是一项繁重的工作。但是,正如我们在使用 DirContextAdapter 简化属性访问和操作中所述, Spring LDAP 为简化这些操作提供了更多帮助。AttributesModificationItem
4. 简化属性访问和操作DirContextAdapter
Java LDAP API 的一个鲜为人知且可能被低估的功能是能够注册 以从找到的 LDAP 条目自动创建对象。 Spring LDAP 利用此功能在某些搜索和查找操作中返回 DirContextAdapter 实例。DirObjectFactory
DirContextAdapter是处理 LDAP 属性的有用工具,尤其是在添加或修改数据时。
4.1. 搜索和查找使用ContextMapper
每当在LDAP树中找到条目时,Spring LDAP都会使用其属性和可分辨名称(DN)来构造. 这允许我们使用 ContextMapper 而不是 来转换找到的值,如下所示:DirContextAdapterAttributesMapper
例 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()); }}如前面的示例所示,我们可以直接按名称检索属性值,而无需通过 and 类。 这在处理多值属性时特别有用。 从多值属性中提取值通常需要循环访问从实现返回的属性值。 为您执行此操作 在 getStringAttributes() 或 getObjectAttributes() 方法中。 下面的示例使用该方法:AttributesAttributeNamingEnumerationAttributesDirContextAdaptergetStringAttributes
例 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 提供了一个抽象的基础实现,称为 AbstractContextMapper。 此实现会自动处理将提供的参数强制转换为 。 因此,使用前面显示的内容可以重写如下:ContextMapperObjectDirContexOperationsAbstractContextMapperPersonContextMapper
例 13.使用AbstractContextMapper
private static class PersonContextMapper extends AbstractContextMapper { public Object doMapFromContext(DirContextOperations ctx) { Person p = new Person(); p.setFullName(ctx.getStringAttribute("cn")); p.setLastName(ctx.getStringAttribute("sn")); p.setDescription(ctx.getStringAttribute("description")); return p; }}4.2. 使用 添加和更新数据DirContextAdapter
` 虽然在提取属性值时很有用,但对于管理详细信息甚至更强大 参与添加和更新数据。DirContextAdapter
4.2.1. 使用 添加数据DirContextAdapter
以下示例用于实现添加数据中介绍的存储库方法的改进实现:DirContextAdaptercreate
例 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); }}请注意,我们使用实例作为绑定的第二个参数,它应该是一个 . 第三个参数是 ,因为我们没有显式指定属性。DirContextAdapterContextnull
另请注意在设置属性值时该方法的使用。 该属性是多值的。类似于提取多值属性数据的麻烦, 构建多值属性是一项繁琐而冗长的工作。通过使用该方法,您可以为您处理这项工作。setAttributeValues()objectclassobjectclasssetAttributeValues()DirContextAdapter
4.2.2. 使用 更新数据DirContextAdapter
我们之前看到使用更新是推荐的方法,但这样做需要我们执行 计算属性修改并相应地构造数组的任务。 可以为我们完成所有这些,如下所示:modifyAttributesModificationItemDirContextAdapter
例 15。更新使用DirContextAdapter
package com.example.repo;public class PersonRepoImpl implements PersonRepo { ... public void update(Person p) { Name dn = buildDn(p); DirContextOperations context = ldapTemplate.lookupContext(dn); context.setAttributeValue("cn", p.getFullname()); context.setAttributeValue("sn", p.getLastname()); context.setAttributeValue("description", p.getDescription()); ldapTemplate.modifyAttributes(context); }}当没有映射器传递给 时,结果是一个实例。 当该方法返回 时,方便的方法方法会自动将返回值强制转换为 (实现的接口)。ldapTemplate.lookup()DirContextAdapterlookupObjectlookupContextDirContextOperationsDirContextAdapter
请注意,和 方法中有重复的代码。此代码从域对象映射到上下文。可以将其提取到单独的方法中,如下所示:createupdate
例 16。使用 DirContextAdapter 添加和修改
package com.example.repo;public class PersonRepoImpl implements PersonRepo { private LdapTemplate ldapTemplate; ... public void create(Person p) { Name dn = buildDn(p); DirContextAdapter context = new DirContextAdapter(dn); context.setAttributeValues("objectclass", new String[] {"top", "person"}); mapToContext(p, context); ldapTemplate.bind(context); } public void update(Person p) { Name dn = buildDn(p); DirContextOperations context = ldapTemplate.lookupContext(dn); mapToContext(person, context); ldapTemplate.modifyAttributes(context); } protected void mapToContext (Person p, DirContextOperations context) { context.setAttributeValue("cn", p.getFullName()); context.setAttributeValue("sn", p.getLastName()); context.setAttributeValue("description", p.getDescription()); }}4.3. 和可分辨名称作为属性值DirContextAdapter
在 LDAP 中管理安全组时,通常具有表示 可分辨名称。由于可分辨名称相等性与字符串相等性不同(例如,空格和大小写差异 在可分辨名称相等中被忽略),使用字符串相等计算属性修改无法按预期工作。
例如,如果一个属性的值为 ,我们调用 , 该属性现在被视为具有两个值,即使字符串实际上表示相同的值 可分辨名称。membercn=John Doe,ou=Peoplectx.addAttributeValue("member", "CN=John Doe, OU=People")
从Spring LDAP 2.0开始,向属性修改方法提供实例可以在计算属性修改时使用可分辨名称相等。如果我们将前面的示例修改为 ,它不会呈现修改,如以下示例所示:javax.naming.NameDirContextAdapterctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People"))
例 17.使用 DirContextAdapter 修改组成员身份
public class GroupRepo implements BaseLdapNameAware { private LdapTemplate ldapTemplate; private LdapName baseLdapPath; public void setLdapTemplate(LdapTemplate ldapTemplate) { this.ldapTemplate = ldapTemplate; } public void setBaseLdapPath(LdapName baseLdapPath) { this.setBaseLdapPath(baseLdapPath); } public void addMemberToGroup(String groupName, Person p) { Name groupDn = buildGroupDn(groupName); Name userDn = buildPersonDn( person.getFullname(), person.getCompany(), person.getCountry()); DirContextOperation ctx = ldapTemplate.lookupContext(groupDn); ctx.addAttributeValue("member", userDn); ldapTemplate.update(ctx); } public void removeMemberFromGroup(String groupName, Person p) { Name groupDn = buildGroupDn(String groupName); Name userDn = buildPersonDn( person.getFullname(), person.getCompany(), person.getCountry()); DirContextOperation ctx = ldapTemplate.lookupContext(groupDn); ctx.removeAttributeValue("member", userDn); ldapTemplate.update(ctx); } private Name buildGroupDn(String groupName) { return LdapNameBuilder.newInstance("ou=Groups") .add("cn", groupName).build(); } private Name buildPersonDn(String fullname, String company, String country) { return LdapNameBuilder.newInstance(baseLdapPath) .add("c", country) .add("ou", company) .add("cn", fullname) .build(); }}在前面的示例中,我们实现获取基本 LDAP 路径,如获取对基本 LDAP 路径的引用中所述。 这是必需的,因为作为成员属性值的可分辨名称必须始终是目录根目录的绝对名称。BaseLdapNameAware
4.4. 一个完整的类PersonRepository
为了说明Spring LDAP和的有用性,以下示例显示了LDAP的完整存储库实现:DirContextAdapterPerson
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) 是使用对象的属性构造的。 在前面的示例中,DN 中使用了国家/地区、公司和全名,这意味着更新这些属性中的任何一个实际上除了更新值之外,还需要使用该操作移动 LDAP 树中的条目。 由于这是高度特定于实现的,因此您需要通过禁止用户更改这些属性或在需要时在您的方法中执行操作来跟踪自己。 请注意,通过使用对象目录映射 (ODM),如果您适当地批注域类,库可以自动为您处理此问题。Personrename()Attributerename()update()
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
需要在托管类上声明 和 注释。 用于指定实体映射到的对象类以及(可选)该类表示的 LDAP 条目的目录根目录。 需要声明映射字段的所有对象类。请注意,在创建托管类的新条目时, 仅使用声明的对象类。@Entry@Id@Entry
为了使目录条目被视为与托管实体匹配,目录条目声明的所有对象类都必须由批注声明。 例如,假定 LDAP 树中有具有以下对象类的条目:。 如果您只对更改对象类中定义的属性感兴趣,则可以使用 . 但是,如果要管理对象类中定义的属性,则需要使用以下属性:。@EntryinetOrgPerson,organizationalPerson,person,topperson@Entry@Entry(objectClasses = { "person", "top" })inetOrgPerson@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" })
批注用于将条目的可分辨名称映射到字段。该字段必须是 的实例。@Idjavax.naming.Name
注释用于将对象类字段映射到实体字段。 需要声明字段映射到的对象类属性的名称,并且可以选择声明 LDAP 属性的语法 OID,以保证精确匹配。 还提供了类型声明,该声明允许您指示 LDAP JNDI 提供程序是将该属性视为基于二进制还是基于字符串。@Attribute@Attribute@Attribute
注释用于将对象类字段映射到条目可分辨名称中的组件或从组件映射对象类字段。 从目录树中读取条目时,批注的字段将自动填充可分辨名称中的相应值。 只能使用 注释类型的字段。不支持其他类型。 如果指定了类中所有注释的属性,则在创建和更新条目时也可以自动计算 DN。 对于更新方案,如果属于可分辨名称的属性已更改,这也会自动处理树中的移动条目。@DnAttribute@DnAttributeString@DnAttributeindex@DnAttribute
注释指示对象目录映射应忽略该字段,而不是映射到底层 LDAP 属性。请注意,如果 a 不绑定到 .也就是说,它只是可分辨名称的一部分,不由对象属性表示。还必须用 注释。@Transient@DnAttributeAttribute@Transient
5.2. 执行
正确配置和注释所有组件后,可以使用 的对象映射方法如下:LdapTemplate
例 18。执行
@Entry(objectClasses = { "person", "top" }, base="ou=someOu")public class Person { @Id private Name dn; @Attribute(name="cn") @DnAttribute(value="cn", index=1) private String fullName; // No @Attribute annotation means this will be bound to the LDAP attribute // with the same value private String description; @DnAttribute(value="ou", index=0) @Transient private String company; @Transient private String someUnmappedField; // ...more attributes below}public class OdmPersonRepo { @Autowired private LdapTemplate ldapTemplate; public Person create(Person person) { ldapTemplate.create(person); return person; } public Person findByUid(String uid) { return ldapTemplate.findOne(query().where("uid").is(uid), Person.class); } public void update(Person person) { ldapTemplate.update(person); } public void delete(Person person) { ldapTemplate.delete(person); } public List<Person> findAll() { return ldapTemplate.findAll(Person.class); } public List<Person> findByLastName(String lastName) { return ldapTemplate.find(query().where("sn").is(lastName), Person.class); }}5.3. ODM and Distinguished Names as Attribute Values
Security groups in LDAP commonly contain a multi-value attribute, where each of the values is the distinguished name of a user in the system. The difficulties involved when handling these kinds of attributes are discussed in DirContextAdapter and Distinguished Names as Attribute Values.
ODM also has support for attribute values, making group modifications easy, as the following example shows:javax.naming.Name
Example 19. Example Group representation
@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); }}通过使用 、 然后调用 来修改组成员时 , 属性修改是通过使用可分辨名称相等性来计算的,这意味着文本格式 在确定可分辨名称是否相等时,会忽略它们。setMembersaddMemberremoveMemberldapTemplate.update()