使用 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 提供了一個名為 AbstractContextMapperContextMapper 抽象基實現。此實現會自動處理將提供的 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),如果您適當地註釋您的域類,該庫可以自動為您處理。
© . This site is unofficial and not affiliated with VMware.