Simplifying Attribute Access and Manipulation with DirContextAdapter

Java LDAP API 的一个鲜为人知(可能被低估)的功能是能够注册一个 @"34" 以从找到的 LDAP 条目中自动创建对象。Spring LDAP 利用此功能在某些搜索和查找操作中返回 @"36" 实例。 DirContextAdapter 是用于处理 LDAP 属性的有用工具,尤其是在添加或修改数据时。

Search and Lookup Using ContextMapper

每当在 LDAP 树中找到一个条目时,Spring LDAP 都会使用它的属性和 Distinguished Name (DN) 来构造一个 @"37"。这使我们能够使用 @"40" 代替 @"39" 来转换找到的值,如下所示:

Example 1. Searching using a 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 实现在 NamingEnumeration 中返回的属性值的循环。DirContextAdaptergetStringAttributes()getObjectAttributes() 方法中执行此操作。以下示例使用 getStringAttributes 方法:

Example 2. Getting multi-value attribute values using 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;
   }
}

Using AbstractContextMapper

Spring LDAP 提供名为 AbstractContextMapperContextMapper 抽象基本实现。此实现会自动负责将提供的 Object 参数强制转换为 DirContexOperations。使用 AbstractContextMapper,前面的 PersonContextMapper 可重写如下:

Example 3. Using an 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;
  }
}

Adding and Updating Data by Using DirContextAdapter

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

Adding Data by Using DirContextAdapter

以下示例使用 DirContextAdapter 来实现 Adding Data 中展示的 create 存储库方法的改进实现:

Example 4. Binding using 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 为你处理这项工作。

Updating Data by Using DirContextAdapter

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

Example 5. Updating using 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 方法中有重复的代码。此代码将域对象映射到上下文。可以将其提取到独立的方法中,如下所示:

Example 6. Adding and modifying using 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 and Distinguished Names as Attribute Values

在 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")),则该示例不会呈现修改,如下例所示:

Example 7. Group Membership Modification using 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 来获取 Obtaining a Reference to the Base LDAP Path 中描述的 LDAP 基础路径。这很重要,因为作为成员属性值的特有名称必须始终是从目录根开始的绝对名称。

A Complete PersonRepository Class

为了说明 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() 操作自行记录此操作(如果需要)。请注意,通过使用 Object-Directory Mapping (ODM),如果对域类进行了适当注释,该库可以自动为您处理此操作。