使用 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 會自動將回傳值轉換為 DirContextOperations (DirContextAdapter 實作的介面)。

請注意,在 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),如果您適當地註解您的網域類別,則程式庫可以自動為您處理此問題。