spring-ldap-core(the Spring LDAP library)
JNDI是用來做LDAP的編程,正如JDBC是用來SQL編程一樣。盡管他們有著完全不同各有優缺點的API,但是它們還是有一些共性:
Spring JDBC提供了jdbcTemplate等簡便的方式來操作數據庫,Spring LDAP也提供了類似的方式操作LDAP----ldapTemplate。
<beanid="contextSourceTarget"class="org.springframework.ldap.core.support.LdapContextSource">
<propertyname="url"value="ldap://localhost:10389" />
<propertyname="base"value="dc=example,dc=com" />
<propertyname="userDn"value="uid=admin,ou=system" />
<propertyname="password"value="secret" />
</bean>
<beanid="contextSource"class="org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy">
<constructor-argref="contextSourceTarget" />
</bean>
<beanid="ldapTemplate"class="org.springframework.ldap.core.LdapTemplate">
<constructor-argref="contextSource" />
</bean>
如上配置後,就可以在程序中注入ldapTemplate進行操作了。
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等。
查詢操作有兩個方法,分別是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屬性的時候代碼更為簡單和優雅。
如上節所示,我們已經知道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只是一些額外的用途,標識取哪些屬性映射到對象中,完全可以忽略這段代碼。
插入數據無非我們要構造這個數據的存儲目錄,和數據屬性,通過上面的知識我們可以很輕松的構造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());
}
}
解綁非常的簡單,直接解綁目錄即可,如下:
ldapTemplate.unbind(dn);
修改即是取得對應Entry,然後修改屬性,通過modifyAttributes方法來修改
DirContextOperations context = ldapTemplate
.lookupContext(dn);
context.setAttributeValue("user-status", status);
ldapTemplate.modifyAttributes(context);
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-argname="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事務配置如下:
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<propertyname="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">
<propertyname="contextSource"ref="contextSource" /><qualifiervalue="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來實現事務管理:
<beanid="ldapJdbcTxManager"class="org.springframework.ldap.transaction.compensating.manager.ContextSourceAndDataSourceTransactionManager">
<propertyname="contextSource"ref="contextSource"/>
<propertyname="dataSource"ref="dataSource" />
<qualifiervalue="ldapJdbcTx"/>
</bean>
正如數據庫操作中的ORM對象關系映射(表到java對象)一樣,ldap操作也有ODM對象目錄映射。
解讀Spring LDAP 幫助中的代碼案例 http://www.linuxidc.com/Linux/2013-07/88083.htm
Spring LDAP 的詳細介紹:請點這裡
Spring LDAP 的下載地址:請點這裡