Simplifying Attribute Access and Manipulation with DirContextAdapter
Java LDAP API 的一个鲜为人知(可能被低估)的功能是能够注册一个 @"34" 以从找到的 LDAP 条目中自动创建对象。Spring LDAP 利用此功能在某些搜索和查找操作中返回 @"36" 实例。
DirContextAdapter
是用于处理 LDAP 属性的有用工具,尤其是在添加或修改数据时。
Search and Lookup Using ContextMapper
每当在 LDAP 树中找到一个条目时,Spring LDAP 都会使用它的属性和 Distinguished Name (DN) 来构造一个 @"37"。这使我们能够使用 @"40" 代替 @"39" 来转换找到的值,如下所示:
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;
}
}
Using AbstractContextMapper
Spring LDAP 提供名为 AbstractContextMapper
的 ContextMapper
抽象基本实现。此实现会自动负责将提供的 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;
}
}
Adding and Updating Data by Using DirContextAdapter
虽然在提取属性值时有用,但 `DirContextAdapter
在管理添加和更新数据所涉及的详细信息方面功能更加强大。
Adding Data by Using DirContextAdapter
以下示例使用 DirContextAdapter
来实现 Adding Data 中展示的 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
为你处理这项工作。
Updating Data by Using 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
and Distinguished Names as Attribute Values
在 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
来获取 Obtaining a Reference to the Base LDAP Path 中描述的 LDAP 基础路径。这很重要,因为作为成员属性值的特有名称必须始终是从目录根开始的绝对名称。
A Complete PersonRepository
Class
为了说明 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) 是使用对象的属性构造的。在前面的示例中, |