JNDI/LDAP和JDBC/DB
JNDI是用来做LDAP的编程,正如JDBC是用来SQL编程一样。尽管他们有着完全不同各有优缺点的API,但是它们还是有一些共性:
- They require extensive plumbing code, even to perform the simplest of tasks.
- All resources need to be correctly closed, no matter what happens.
- Exception handling is difficult
Spring JDBC提供了jdbcTemplate等简便的方式来操作数据库,Spring LDAP也提供了类似的方式操作LDAP----ldapTemplate。
xml 配置(LdapContextSource、ldapTemplate)
<bean id="contextSourceTarget" class="org.springframework.ldap.core.support.LdapContextSource"> <property name="url" value="ldap://localhost:10389" /> <property name="base" value="dc=example,dc=com" /> <property name="userDn" value="uid=admin,ou=system" /> <property name="password" value="secret" /> </bean> <bean id="contextSource" class="org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy"> <constructor-arg ref="contextSourceTarget" /> </bean> <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate"> <constructor-arg ref="contextSource" /> </bean>
如上配置后,就可以在程序中注入ldapTemplate进行操作了。
使用DistinguishedName来构建DN,使用过滤器来更多的限制查询条件
DistinguishedName类实现了Name接口,提供了更优秀的方法来操作目录,这个类已经过时了,推荐使用javax.naming.ldap.LdapName这个类。在我们构建任何操作的时候,都可以使用此类构造基目录。
private DistinguishedName buildDn() { DistinguishedName dn = new DistinguishedName(); dn.append("ou", "People"); return dn; }
一个更复杂的构建如下,查找某个唯一userid的dn,其中使用了过滤器AndFilter:
protected Name getPeopleDn(String userId) { AndFilter andFilter = new AndFilter(); andFilter.and(new EqualsFilter("objectclass", "person")); andFilter.and(new EqualsFilter("objectclass", "xUserObjectClass")); andFilter.and(new EqualsFilter("uid", userId)); List<Name> result = ldapTemplate.search(buildDn(), andFilter.encode(), SearchControls.SUBTREE_SCOPE, new AbstractContextMapper() { @Override protected Name doMapFromContext(DirContextOperations adapter) { return adapter.getDn(); } }); if (null == result || result.size() != 1) { throw new UserNotFoundException(); } else { return result.get(0); } }
除了dn能限制目录条件外,过滤器提供了关于属性的查询限制条件,AndFilter是与的过滤器,EqualsFilter则是相等过滤器,还有很多内 置过滤器,如WhitespaceWildcardsFilter、LikeFilter、GreaterThanOrEqualsFilter、 LessThanOrEqualsFilter等。
稍复杂的AttributesMapper---查询(search、lookup)
查询操作有两个方法,分别是search和lookup,前者在不知道dn的情况下进行搜索,而后者更像是直接取出对应的Entry。如上的search 代码就是在某个dn的所有子树(SearchControls.SUBTREE_SCOPE)下搜索符合过滤器条件的DN列表:
List<DistinguishedName> result = ldapTemplate.search(buildDn(), filter.encode(), SearchControls.SUBTREE_SCOPE, new AbstractContextMapper() { @Override protected DistinguishedName doMapFromContext( DirContextOperations adapter) { return (DistinguishedName) adapter.getDn(); } });
下面的代码将是直接查出所需要的Entry,其中第二个参数表示要取出的属性,可选:
public Account queryUser(String userId) { return (Account) ldapTemplate.lookup(getPeopleDn(userId), new String[] { "uid", "cn", "objectClass" }, new AccountAttributesMapper()); } private class AccountAttributesMapper implements AttributesMapper { public Object mapFromAttributes(Attributes attrs) throws NamingException { Account person = new Account(); person.setUserID((String)attrs.get("uid").get()); person.setUserName((String)attrs.get("cn").get()); person.setDescription((String)attrs.get("description").get()); return person; } }
AttributesMapper类似与JDBC中的RowMapper,实现这个接口可以实现ldap属性到对象的映射,spring也提供了更为简单 的上下文映射AbstractContextMapper来实现映射,这个类在取ldap属性的时候代码更为简单和优雅。
更简单的上下文映射AbstractContextMapper---查询
如上节所示,我们已经知道ldap属性到对象的映射,在我们查找对象时,我们可以使映射更为简单,如下:
public Account queryUser(String userId) { return (Account) ldapTemplate.lookup(getPeopleDn(userId), new String[] { "uid", "cn", "objectClass" }, new AccountContextMapper()); } private class AccountContextMapper extends AbstractContextMapper { private String[] ldapAttrId; @SuppressWarnings("unchecked") public AccountContextMapper() { ldapAttrId = buildAttr(userAttrService.getLdapAttrIds(), new String[] { "uid", "cn", "objectClass" }); } @Override public Object doMapFromContext(DirContextOperations context) { Account account = new Account(); account.setUserId(context.getStringAttribute("uid")); account.setUserName(context.getStringAttribute("cn")); Map<String, Object> userAttributes = new HashMap<String, Object>(); // 取帐号元数据的属性值 for (String ldapAttr : ldapAttrId) { Object value; Object[] values = context.getObjectAttributes(ldapAttr); if (values == null || values.length == 0) continue; value = (values.length == 1) ? values[0] : values; if (value instanceof String && StringUtils.isEmpty(value.toString())) continue; userAttributes.put(ldapAttr, value); } account.setUserAttributes(userAttributes); return account; } }
在上面的代码中,我们完全可以只关注doMapFromContext这个方法,通过参数context让获取属性更为方便,其中的变量ldapAttrId只是一些额外的用途,标识取哪些属性映射到对象中,完全可以忽略这段代码。
增加(binding)
插入数据无非我们要构造这个数据的存储目录,和数据属性,通过上面的知识我们可以很轻松的构造DN,构造数据我们采用DirContextAdapter这个类,代码如下:
DirContextAdapter context = new DirContextAdapter(dn); context.setAttributeValues("objectclass", userLdapObjectClasses.split(",")); context.setAttributeValue("uid", account.getUserId()); mapToContext(account, context); ldapTemplate.bind(context);
ldapTemplate.bind(context)是绑定的核心api。
ldap的属性值也是有类型的,比如可以是字符串,则通过setAttributeValue来设置属性值,可以是数组,则通过 setAttributeValues来设置属性值。其中mapToContext属于自定义的方法,用来映射更多的对象属性到LDAP属性,如下自定义 的代码:
protected void mapToContext(Account account, DirContextOperations ctx) { ctx.setAttributeValue("cn", account.getUserName()); ctx.setAttributeValue("sn", account.getUserName()); ctx.setAttributeValue("user-account-time", getDateStr(account.getLifeTime(), "yyyy/MM/dd")); if (StringUtils.isNotEmpty(account.getPassword())) { ctx.setAttributeValue("userPassword", account.getPassword()); } Map<String, Object> userAttributes = account.getUserAttributes(); for (Map.Entry<String, Object> o : userAttributes.entrySet()) { String ldapAtt = userAttrService.getLdapAttrId(o.getKey()); if (ldapAtt == null) throw new RuntimeException("Invalid attribute " + o.getKey()); if (o.getValue() == null) continue; if (o.getValue() instanceof String && StringUtils.isWhitespace(o.getValue().toString())) { continue; } if (ObjectUtils.isArray(o.getValue()) && !(o.getValue() instanceof byte[])) { Object[] array = ObjectUtils.toObjectArray(o.getValue()); if (array != null && array.length != 0) ctx.setAttributeValues(ldapAtt, array); } else if (o.getValue() instanceof List) { Object[] array = ((List) o.getValue()).toArray(); if (array != null && array.length != 0) ctx.setAttributeValues(ldapAtt, array); } else ctx.setAttributeValue(ldapAtt, o.getValue()); } }
删除(unbinding)
解绑非常的简单,直接解绑目录即可,如下:
ldapTemplate.unbind(dn);
修改(modifying)
修改即是取得对应Entry,然后修改属性,通过modifyAttributes方法来修改
DirContextOperations context = ldapTemplate .lookupContext(dn); context.setAttributeValue("user-status", status); ldapTemplate.modifyAttributes(context);
rename(重命名或者移动Entry)
Spring 支持移动Entry,通过rename方法,与其同时带来了一个概念:移动策略或者重命名策略。Spring内置了两个策略,分别是 DefaultTempEntryRenamingStrategy和 DifferentSubtreeTempEntryRenamingStrategy,前者策略是在条目的最后增加后缀用来生成副本,对于 DNcn=john doe, ou=users, 这个策略将生成临时DN cn=john doe_temp, ou=users.后缀可以通过属性tempSuffix来配置,默认是"_temp",
后者策略则是在一个单独的dn下存放临时dn,单独的dn可以通过属性subtreeNode来配置。 策略配置在ContextSourceTransactionManager的事务管理Bean中,属性名为renamingStrategy。 如:
<bean id="ldapRenameStrategy" class="org.springframework.ldap.transaction.compensating.support.DifferentSubtreeTempEntryRenamingStrategy" > <constructor-arg name="subtreeNode" value="ou=People,dc=temp,dc=com,dc=cn"></constructor-arg> </bean>
上面这么多,需要在ds服务器支持移动的条件下,否则只能通过删除--插入来代替移动,如下是个移动的示例:
DirContextAdapter newContext = new DirContextAdapter(newDn); DirContextOperations oldContext = ldapTemplate.lookupContext(oldDn); NamingEnumeration<? extends Attribute> attrs = oldContext .getAttributes().getAll(); try { while (attrs.hasMore()) { Attribute attr = attrs.next(); newContext.getAttributes().put(attr); } } catch (NamingException e) { throw new RuntimeException("remove entry error:" + e.getMessage()); } ldapTemplate.unbind(oldDn); ldapTemplate.bind(newContext);
分页支持
很可惜,有些ldap服务器不支持分页,而有些已经支持PagedResultsControl,可以通过cookie来实现与ldap的分页交互。官方文档的示例如下:
public PagedResult getAllPersons(PagedResultsCookie cookie) { PagedResultsRequestControl control = new PagedResultsRequestControl(PAGE_SIZE, cookie); SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); List persons = ldapTemplate.search("", "objectclass=person", searchControls, control); return new PagedResult(persons, control.getCookie()); }
JDBC 和 LDAP 的事务管理
事务在项目中是必须考虑的一部分,这节讨论两种事务的分别处理和结合,通过注解来完成。 典型的JDBC事务配置如下:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven transaction-manager="txManager" />
我们配置了jdbc的默认事务管理为txManager,在服务层我们可以使用注解@Transcational来标注事务。
在单独需要ldap事务管理时,我们可以配置ldap的事务,起了个别名ldapTx:
<bean id="ldapTxManager" class="org.springframework.ldap.transaction.compensating.manager.ContextSourceTransactionManager"> <property name="contextSource" ref="contextSource" /> <qualifier value="ldapTx"/> </bean>
我们可以使用注解@Transactional("ldapTx")来标注ldap的事务,如果不想每次引用别名,使用@LdapTransactional,则可以创建注解:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.transaction.annotation.Transactional; @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Transactional("ldapTx") public @interface LdapTransactional { }
在ldap和jdbc同时都有操作的服务中,我们可以配置ContextSourceAndDataSourceTransactionManager来实现事务管理:
<bean id="ldapJdbcTxManager" class="org.springframework.ldap.transaction.compensating.manager.ContextSourceAndDataSourceTransactionManager"> <property name="contextSource" ref="contextSource"/> <property name="dataSource" ref="dataSource" /> <qualifier value="ldapJdbcTx"/> </bean>
Object-Directory Mapping (ODM) 简介
正如数据库操作中的ORM对象关系映射(表到java对象)一样,ldap操作也有ODM对象目录映射。
springLdap 操作ldap示例(增删改查)
在看这个文章之前,最好是了解了openldap的schema文件,也就是了解objectClass和attribute以及它们的关系。否则很容易不了解代码的含义以及抛出的异常。
package ldap.entity;
/**
* 本测试类person对象来自schema文件的core.schema文件
* objectClass为person,必填属性和可选属性也是根据该对象得到的。
* Author:Ding Chengyun
*/
public class Person {
private String sn; //必填属性
private String cn; //必填属性
private String userPassword; //可选属性
private String telephoneNumber; //可选属性
private String seeAlso; //可选属性
private String description; //可选属性
public String getSn() {
return sn;
}
public void setSn(String sn) {
this.sn = sn;
}
public String getCn() {
return cn;
}
public void setCn(String cn) {
this.cn = cn;
}
public String getUserPassword() {
return userPassword;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public String getTelephoneNumber() {
return telephoneNumber;
}
public void setTelephoneNumber(String telephoneNumber) {
this.telephoneNumber = telephoneNumber;
}
public String getSeeAlso() {
return seeAlso;
}
public void setSeeAlso(String seeAlso) {
this.seeAlso = seeAlso;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
package ldap.mapper;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import ldap.entity.Person;
import org.springframework.ldap.core.AttributesMapper;
/**
* 这个类的作用是将ldap中的属性转化为实体类的属性值,
* 在查询信息的时候会用到
*/
public class PersonAttributeMapper implements AttributesMapper{
@Override
public Object mapFromAttributes(Attributes attr) throws NamingException {
Person person = new Person();
person.setSn((String)attr.get("sn").get());
person.setCn((String)attr.get("cn").get());
if (attr.get("userPassword") != null) {
person.setUserPassword((String)attr.get("userPassword").get());
}
if (attr.get("telephoneNumber") != null) {
person.setTelephoneNumber((String)attr.get("telephoneNumber").get());
}
if (attr.get("seeAlso") != null) {
person.setSeeAlso((String)attr.get("seeAlso").get());
}
if (attr.get("description") != null) {
person.setDescription((String)attr.get("description").get());
}
return person;
}
}
package ldap.dao;
import java.util.ArrayList;
import java.util.List;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import ldap.entity.Person;
import ldap.mapper.PersonAttributeMapper;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import xhrd.ucenter.ldap.entity.UcenterLdapApplication;
import xhrd.ucenter.ldap.ldapAttributeMappper.ApplicationAttributeMapper;
/**
* Description: 此类的作用是使用spring的 LdapTemplate完成对ldap的增删改查的操作
* Author:Ding Chengyun
*/
public class PersonDao {
//注入spring的LdapTemplate,此处在spring的配置文件中需要配置
private LdapTemplate ldapTemplate;
public LdapTemplate getLdapTemplate() {
return ldapTemplate;
}
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
/**
* 添加 一条记录
* @param person
*/
public void createOnePerson(Person person) {
BasicAttribute ba = new BasicAttribute("objectclass");
ba.add("person"); //此处的person对应的是core.schema文件中的objectClass:person
Attributes attr = new BasicAttributes();
attr.put(ba);
//必填属性,不能为null也不能为空字符串
attr.put("cn", person.getCn());
attr.put("sn", person.getSn());
//可选字段需要判断是否为空,如果为空则不能添加
if (person.getDescription() != null
&& person.getDescription().length() > 0) {
attr.put("description", person.getDescription());
}
if (person.getUserPassword() != null
&& person.getUserPassword().length() > 0) {
attr.put("userPassword", person.getUserPassword());
}
if (person.getSeeAlso() != null
&& person.getSeeAlso().length() > 0) {
attr.put("seeAlso", person.getSeeAlso());
}
if (person.getTelephoneNumber() != null
&& person.getTelephoneNumber().length() > 0) {
attr.put("telephoneNumber", person.getTelephoneNumber());
}
//bind方法即是添加一条记录。
ldapTemplate.bind(getDn(person.getCn()), null, attr);
}
/**
/**
* 根据dn查询详细信息
* @param cn
* @return
*/
public UcenterLdapApplication getPersonDetail(String cn) {
try {
//ldapTeplate的lookup方法是根据dn进行查询,此查询的效率超高
UcenterLdapApplication ua = (UcenterLdapApplication)
ldapTemplate.lookup(getDn(cn),
new ApplicationAttributeMapper());
return ua;
} catch (NameNotFoundException e) {
return null;
}
}
/**
* 根据自定义的属性值查询person列表
* @param person
* @return
*/
public List<Person> getPersonList(
Person person) {
List<Person> list = new ArrayList<Person>();
//查询过滤条件
AndFilter andFilter = new AndFilter();
andFilter.and(new EqualsFilter("objectclass", "person"));
if (person.getCn() != null
&& person.getCn().length() > 0) {
andFilter.and(new EqualsFilter("cn", person.getCn()));
}
if (person.getSn() != null
&& person.getSn().length() > 0) {
andFilter.and(new EqualsFilter("sn", person.getSn()));
}
if (person.getDescription() != null
&& person.getDescription().length() > 0) {
andFilter.and(new EqualsFilter("description", person.getDescription()));
}
if (person.getUserPassword() != null
&& person.getUserPassword().length() > 0) {
andFilter.and(new EqualsFilter("userPassword", person.getUserPassword()));
}
if (person.getSeeAlso() != null
&& person.getSeeAlso().length() > 0) {
andFilter.and(new EqualsFilter("seeAlso", person.getSeeAlso()));
}
if (person.getTelephoneNumber() != null
&& person.getTelephoneNumber().length() > 0) {
andFilter.and(new EqualsFilter("telephoneNumber", person.getTelephoneNumber()));
}
//search是根据过滤条件进行查询,第一个参数是父节点的dn,可以为空,不为空时查询效率更高
list = ldapTemplate.search("", andFilter.encode(),
new PersonAttributeMapper());
return list;
}
/**
* 删除一条记录,根据dn
* @param cn
*/
public void removeOnePerson(String cn) {
ldapTemplate.unbind(getDn(cn));
}
/**
* 修改操作
* @param person
*/
public void updateOnePerson(Person person) {
if (person == null || person.getCn() == null
|| person.getCn().length() <= 0) {
return;
}
List<ModificationItem> mList = new ArrayList<ModificationItem>();
mList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("sn",person.getSn())));
mList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("description",person.getDescription())));
mList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("seeAlso",person.getSeeAlso())));
mList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("telephoneNumber",person.getTelephoneNumber())));
mList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userPassword",person.getUserPassword())));
if (mList.size() > 0) {
ModificationItem[] mArray = new ModificationItem[mList.size()];
for (int i = 0; i < mList.size(); i++) {
mArray[i] = mList.get(i);
}
//modifyAttributes 方法是修改对象的操作,与rebind()方法需要区别开
ldapTemplate.modifyAttributes(this.getDn(person.getCn()), mArray);
}
}
/**
* 得到dn
* @param cn
* @return
*/
private DistinguishedName getDn(String cn) {
//得到根目录,也就是配置文件中配置的ldap的根目录
DistinguishedName newContactDN = new DistinguishedName();
// 添加cn,即使得该条记录的dn为"cn=cn,根目录",例如"cn=abc,dc=testdc,dc=com"
newContactDN.add("cn", cn);
return newContactDN;
}
}
Spring-ldap Filter案列详解
org.springframework.ldap.filter.AndFilter :且
org.springframework.ldap.filter.OrFilter :或者
org.springframework.ldap.filter.NotFilter :非
org.springframework.ldap.filter.PresentFilter :LDAP目中有存储属性的
org.springframework.ldap.filter.NotPresentFilter :LDAP目中有无存储属性的
org.springframework.ldap.filter.EqualsFilter :等于
org.springframework.ldap.filter.LikeFilter :等于
org.springframework.ldap.filter.WhitespaceWildcardsFilter : 模糊
org.springframework.ldap.filter.GreaterThanOrEqualsFilter :大于等于
org.springframework.ldap.filter.LessThanOrEqualsFilter : 小于等于
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.GreaterThanOrEqualsFilter;
import org.springframework.ldap.filter.LessThanOrEqualsFilter;
import org.springframework.ldap.filter.LikeFilter;
import org.springframework.ldap.filter.NotFilter;
import org.springframework.ldap.filter.NotPresentFilter;
import org.springframework.ldap.filter.OrFilter;
import org.springframework.ldap.filter.PresentFilter;
import org.springframework.ldap.filter.WhitespaceWildcardsFilter;
public class LdapFilterTest {
/**
* @param args
*/
public static void main(String[] args) {
// 且
AndFilter filter = new AndFilter();
// 等于
filter.and(new EqualsFilter("objectclass", "person"));
// 模糊
filter.and(new WhitespaceWildcardsFilter("sn", "张三"));
System.out.println(filter.encode());
AndFilter filter2 = new AndFilter();
// 等于
filter2.and(new LikeFilter("cn","12121"));
filter2.and(filter);
System.out.println(filter2.encode());
AndFilter filter3 = new AndFilter();
// 小于等于
filter3.and(new LessThanOrEqualsFilter("age","40"));
filter3.and(filter2);
System.out.println(filter3.encode());
AndFilter filter4 = new AndFilter();
// 大于等于
filter4.and(new GreaterThanOrEqualsFilter("age","20"));
filter4.and(filter3);
System.out.println(filter4.encode());
// 或者
OrFilter filter5 = new OrFilter();
filter5.or(new LikeFilter("cn","12120") );
filter5.or(new WhitespaceWildcardsFilter("sn", "张三"));
System.out.println(filter5.encode());
// 非
NotFilter filter6 = new NotFilter(new LikeFilter("cn","12120"));
System.out.println(filter6.encode());
// LDAP目中有无存储属性的
NotPresentFilter filter7 = new NotPresentFilter("desc");
System.out.println(filter7.encode());
// LDAP目中有存储属性的
PresentFilter filter8 = new PresentFilter("email");
System.out.println(filter8.encode());
}
}
来源:http://my.oschina.net/sayi/blog/180123