简化 DirContextAdapter 的属性访问和操作

一个鲜为人知(也许被高估了)的Java LDAP API特性是能够注册一个DirObjectFactory,以自动从找到的LDAP条目创建对象。 Spring LDAP在某些搜索和查找操作中利用了这一特性来返回DirContextAdapter实例。spring-doc.cadn.net.cn

DirContextAdapter 是一个用于处理 LDAP 属性的有用工具,特别是在添加或修改数据时。spring-doc.cadn.net.cn

搜索和查找使用ContextMapper

当 一个 entry 被 发现 在 LDAP 树 中,它的 attributes 和 Distinguished Name (DN) 会被 Spring LDAP 用来 构造 一个 DirContextAdapter。 这让我们可以 使用 一个 ContextMapper 代替 一个 AttributesMapper 来 转换 找到 的 值,如下 所示:spring-doc.cadn.net.cn

示例 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类。 这在处理多值属性时特别有用。 从多值属性中提取值通常需要遍历NamingEnumeration,即从Attributes实现返回的属性值集合。 DirContextAdapter会为你处理此操作 在getStringAttributes()getObjectAttributes()方法中。 以下示例使用getStringAttributes方法:spring-doc.cadn.net.cn

示例 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 可以重写如下:spring-doc.cadn.net.cn

示例 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

While useful when extracting attribute values, DirContextAdapter is even more powerful for managing the details involved in adding and updating data.spring-doc.cadn.net.cn

添加数据使用DirContextAdapter

以下示例使用DirContextAdapter来实现create仓库方法的改进实现,该方法在添加数据中介绍:spring-doc.cadn.net.cn

示例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,因为我们没有显式指定属性。spring-doc.cadn.net.cn

也请注意在设置objectclass属性值时使用setAttributeValues()方法。 objectclass属性是多值。类似地,提取多值属性数据也会遇到麻烦, 构建多值属性是一项繁琐且冗长的工作。通过使用setAttributeValues()方法,可以由DirContextAdapter为您处理这项工作。spring-doc.cadn.net.cn

更新数据使用DirContextAdapter

我们之前看到,使用 modifyAttributes 进行更新是推荐的做法,但这需要我们执行计算属性修改并相应地构建 ModificationItem 数组的任务。 DirContextAdapter 可以帮我们完成所有这些工作,如下所示:spring-doc.cadn.net.cn

示例 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 方法返回 ObjecttoEntry 会自动将返回值转换为 DirContextOperationsDirContextAdapter 所实现的接口)。spring-doc.cadn.net.cn

请注意在LdapTemplate#createLdapTemplate#update方法中存在重复代码。这段代码将领域对象映射到上下文。可以提取到一个单独的方法中,如下所示:spring-doc.cadn.net.cn

示例 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 管理安全组时,经常会遇到属性值表示区分名。由于区分名的相等性与字符串相等性不同(例如,区分名相等性会忽略空格和大小写差异),使用字符串相等性来计算属性修改将不会产生预期的效果。spring-doc.cadn.net.cn

例如,如果一个member属性的值为cn=John Doe,ou=People,当我们调用ctx.addAttributeValue("member", "CN=John Doe, OU=People"), 该属性现在被认为具有两个值,即使字符串实际上代表的是同一个 区分名。"spring-doc.cadn.net.cn

As of Spring LDAP 2.0, supplying javax.naming.Name instances to the attribute modification methods makes DirContextAdapter use distinguished name equality when calculating attribute modifications. If we modify the earlier example to be ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People")), it does not render a modification, as the following example shows:spring-doc.cadn.net.cn

示例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路径。 这有必要,因为作为成员属性值的区分名称必须始终从目录根绝对化。spring-doc.cadn.net.cn

一个完整的PersonRepository

为了说明Spring LDAP和DirContextAdapter的有用性,以下示例展示了一个完整的Person Repository在LDAP中的实现:spring-doc.cadn.net.cn

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对象的国家、公司和全名,这意味着更新这些属性中的任何一个实际上需要使用rename()操作将LDAP树中的条目移动,而不仅仅是更新Attribute值。 由于这是高度实现特定的,因此这需要你自己跟踪,可以通过不允许用户更改这些属性,或者在需要时在你的update()方法中执行rename()操作来处理。 请注意,通过使用对象目录映射(ODM),库可以在你适当注解领域类的情况下自动为你处理此事。