使用 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
例項用作 bind 方法的第二個引數,該引數應該是 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
的有用性,以下示例展示了一個完整的 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),如果您對領域類進行適當的註解,庫可以自動為您處理此問題。 |