使用 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 提供了一个名为 AbstractContextMapper 的 ContextMapper 抽象基实现。此实现会自动处理将提供的 Object 参数转换为 DirContexOperations。使用 AbstractContextMapper,前面显示的 PersonContextMapper 可以重写如下
AbstractContextMapperprivate 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),如果您适当地注释您的域类,该库可以自动为您处理。 |