使用 DirContextAdapter 简化属性访问和操作

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

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

使用 ContextMapper 进行搜索和查找

每当在 LDAP 树中找到一个条目时,Spring LDAP 会使用其属性和可分辨名称 (DN) 来构建一个 DirContextAdapter。这使我们能够使用 ContextMapper 而不是 AttributesMapper 来转换找到的值,如下所示

示例 1. 使用 ContextMapper 进行搜索
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 ldapClient.search().name(dn).toObject(new PersonContextMapper());
   }
}

如前例所示,我们可以直接按名称检索属性值,而无需通过 AttributesAttribute 类。这在处理多值属性时特别有用。从多值属性中提取值通常需要循环遍历从 Attributes 实现返回的属性值的 NamingEnumerationDirContextAdaptergetStringAttributes()getObjectAttributes() 方法中为您执行此操作。以下示例使用 getStringAttributes 方法

示例 2. 使用 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;
   }
}

使用 AbstractContextMapper

Spring LDAP 提供了 ContextMapper 的抽象基实现,称为 AbstractContextMapper。此实现自动处理将提供的 Object 参数转换为 DirContexOperations。使用 AbstractContextMapper,前面显示的 PersonContextMapper 可以改写如下

示例 3. 使用 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;
  }
}

使用 DirContextAdapter 添加和更新数据

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

使用 DirContextAdapter 添加数据

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

示例 4. 使用 DirContextAdapter 绑定
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());

      ldapClient.bind(dn).object(context).execute();
   }
}

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

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

使用 DirContextAdapter 更新数据

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

示例 5. 使用 DirContextAdapter 更新
public class PersonRepoImpl implements PersonRepo {
   ...
   public void update(Person p) {
      Name dn = buildDn(p);
      DirContextOperations context = ldapClient.search().name(dn).toEntry();

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

      ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
   }
}

调用 SearchSpec#toEntry 时,结果默认情况下为 DirContextAdapter 实例。虽然 lookup 方法返回一个 Object,但 toEntry 会自动将返回值转换为 DirContextOperationsDirContextAdapter 实现的接口)。

请注意,我们在 LdapTemplate#createLdapTemplate#update 方法中存在重复代码。这段代码将域对象映射到上下文。它可以提取到一个单独的方法中,如下所示

示例 6. 使用 DirContextAdapter 添加和修改
public class PersonRepoImpl implements PersonRepo {
   private LdapClient ldapClient;

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

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

   public void update(Person p) {
      Name dn = buildDn(p);
      DirContextOperations context = ldapClient.search().name(dn).toEntry();
      mapToContext(person, context);
      ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
   }

   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 LDAP 2.0 开始,向属性修改方法提供 javax.naming.Name 实例会使 DirContextAdapter 在计算属性修改时使用区分名称相等性。如果我们将前面的示例修改为 ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People")),它将**不会**呈现修改,如下例所示

示例 7. 使用 DirContextAdapter 的组成员资格修改
public class GroupRepo implements BaseLdapNameAware {
    private LdapClient ldapClient;
    private LdapName baseLdapPath;

    public void setLdapClient(LdapClient ldapClient) {
        this.ldapClient = ldapClient;
    }

    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 = ldapClient.search().name(groupDn).toEntry();
        ctx.addAttributeValue("member", userDn);

        ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute();
    }

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

        DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry();
        ctx.removeAttributeValue("member", userDn);

        ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute();
    }

    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 路径的引用 中所述。这是必需的,因为作为成员属性值的区分名称必须始终从目录根目录开始。

完整的 PersonRepository

为了说明 Spring LDAP 和 DirContextAdapter 的实用性,以下示例展示了用于 LDAP 的完整 Person 存储库实现

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 LdapClient ldapClient;

   public void setLdapClient(LdapClient ldapClient) {
      this.ldapClient = ldapClient;
   }

   public void create(Person person) {
      DirContextAdapter context = new DirContextAdapter(buildDn(person));
      mapToContext(person, context);
      ldapClient.bind(context.getDn()).object(context).execute();
   }

   public void update(Person person) {
      Name dn = buildDn(person);
      DirContextOperations context = ldapClient.lookupContext(dn);
      mapToContext(person, context);
      ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
   }

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

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

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

      return ldapClient.search().query(query).toList(getContextMapper());
   }

   public List<Person> findAll() {
      EqualsFilter filter = new EqualsFilter("objectclass", "person");
      return ldapClient.search().query((query) -> query.filter(filter)).toList(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() 操作将条目移动到 LDAP 树中,除了更新 Attribute 值。由于这是高度特定于实现的,因此您需要自己跟踪,要么不允许用户更改这些属性,要么在需要时在您的 update() 方法中执行 rename() 操作。请注意,通过使用 对象目录映射 (ODM),如果适当地注释您的域类,库可以自动为您处理此操作。