使用 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),如果您对领域类进行适当的注解,库可以自动为您处理此问题。 |