使用 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 实例用作 bind 方法的第二个参数,该参数应该是 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 的有用性,以下示例展示了一个完整的 Person 仓库实现,用于 LDAP:

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),如果您对领域类进行适当的注解,库可以自动为您处理此问题。