使用 DirContextAdapter
簡化屬性存取和操作
Java LDAP API 一個鮮為人知且可能被低估的功能是註冊 DirObjectFactory
以自動從找到的 LDAP 條目建立物件的能力。Spring LDAP 利用此功能在某些搜尋和查詢操作中回傳 DirContextAdapter
實例。
DirContextAdapter
是一個用於處理 LDAP 屬性的實用工具,尤其是在新增或修改資料時。
使用 ContextMapper
進行搜尋和查詢
每當在 LDAP 樹狀結構中找到條目時,Spring LDAP 就會使用其屬性和辨別名稱 (DN) 來建構 DirContextAdapter
。這讓我們可以使用 ContextMapper
而不是 AttributesMapper
來轉換找到的值,如下所示
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());
}
}
如上述範例所示,我們可以透過名稱直接檢索屬性值,而無需經過 Attributes
和 Attribute
類別。這在處理多值屬性時特別有用。從多值屬性中提取值通常需要迴圈遍歷從 Attributes
實作回傳的屬性值的 NamingEnumeration
。DirContextAdapter
在 getStringAttributes()
或 getObjectAttributes()
方法中為您執行此操作。以下範例使用 getStringAttributes
方法
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
可以改寫如下
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
儲存庫方法的改進實作
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
可以為我們完成所有這些工作,如下所示
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#create
和 LdapTemplate#update
方法中,我們有重複的程式碼。此程式碼從網域物件映射到內容。它可以提取到一個單獨的方法中,如下所示
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"))
,則它不會呈現修改,如下列範例所示
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),如果您適當地註解您的網域類別,則程式庫可以自動為您處理此問題。 |