6- 章的翻譯見:http://www.reibang.com/p/835c2db4a1c4
說明
該文檔根據(jù)官方英文文檔翻譯而來寞埠,該文檔是對(duì)spring-ldap-core的2.3.2.RELEASE版本的介紹萝喘。翻譯如下嫁赏。
Spring LDAP 參考文檔
Mattias Hellborg Arthursson,Ulrik Sandberg肤视,Eric Dalquist赂弓,Keith Barlow,Rob Winch孽鸡。2.3.2.RELEASE版本蹂午。
Spring LDAP使得我們構(gòu)建基于Spring的那些使用了LDAP協(xié)議的應(yīng)用變得簡(jiǎn)單
假如你不把本文檔設(shè)為收費(fèi)閱讀栏豺,甚至你在提供本文內(nèi)容時(shí)在文檔中包含此版權(quán)說明,那你可以以紙質(zhì)版或電子版的形式拷貝本文的副本來自己使用或做他用
前言
JDNI( Java Naming and Directory Interface)之于LDAP編程就像JDBC( Java Database Connectivity)之于SQL編程豆胸。在JDBC與JNDI/LDAP之間有些相似之處奥洼。盡管它們是兩套有著各自優(yōu)缺點(diǎn)的API,但它們都有一下一些不討人喜歡的特點(diǎn):
- 它們都要求我們寫大量業(yè)務(wù)無關(guān)的代碼晚胡,即便我們只是要寫一個(gè)很簡(jiǎn)單的功能
- 無論程序運(yùn)行情況灵奖,都要求我們正確的關(guān)閉資源
- 對(duì)異常的處理不友好
以上幾點(diǎn)導(dǎo)致我們?cè)谑褂眠@些API時(shí)出現(xiàn)大量重復(fù)的代碼。我們都知道估盘,重復(fù)代碼是最糟糕的代碼風(fēng)格之一瓷患。總而言之遣妥,我們可以總結(jié)為:在JAVA程序中使用JDBC與LDAP編程會(huì)變得極度無聊與啰嗦擅编。
注:個(gè)人感覺原文文檔有筆誤,按照前后文的意思箫踩,應(yīng)該是使用JDBC與JNDI直接操作來編程會(huì)很無聊與啰嗦
Spring JDBC爱态,Spring框架的核心組件,對(duì)SQL編程提供了非常實(shí)用的功能境钟。同樣地锦担,我們需要一個(gè)類似的框架來簡(jiǎn)化JAVA中的LDAP編程。
1.引言
1.1 概述
設(shè)計(jì)Spring LDAP是為了簡(jiǎn)化JAVA中的LDAP操作慨削。以下是該lib提供的功能:
- JdbcTemplate:為L(zhǎng)DAP編程設(shè)計(jì)了一個(gè)簡(jiǎn)化的模版
- JPA/Hibernate:設(shè)計(jì)了基于注解的對(duì)象/目錄映射
- Spring Data repository的支持洞渔,包括對(duì)QueryDSL的支持
- 簡(jiǎn)化構(gòu)建LDAP查詢與LDAP DN的功能
- LDAP連接池
- 客戶端LDAP補(bǔ)償事務(wù)的支持
1.2傳統(tǒng)的JAVA LDAP操作 V/S 使用LdapTemplate
我們考慮這么一個(gè)方法:查詢數(shù)據(jù)中所有的Person并將它們的name屬性添加到list中,返回這個(gè)list缚态。使用JDBC的話痘煤,我們要?jiǎng)?chuàng)建一個(gè)連接connection,并使用statement來創(chuàng)建一個(gè)查詢猿规,在返回的結(jié)果集result set中雷厂,我們循環(huán)每一條結(jié)果掩宜,并取出我們需要的那一列中的內(nèi)容异赫,將該內(nèi)容放入list中庶溶。
類似的诱贿,我們使用JNDI來操作LDAP數(shù)據(jù)庫的話添忘,需要?jiǎng)?chuàng)建一個(gè)context算柳,并使用查詢過濾器search filter來進(jìn)行查詢嗜愈。然后我們會(huì)循環(huán)返回的naming enumeration并取出我們想要的屬性attribute宝冕,將該屬性的內(nèi)容放入list中张遭。
傳統(tǒng)的使用JAVA JNDI來實(shí)現(xiàn)上述查詢方法的代碼如下所示。注意粗體的代碼地梨,這些代碼是真正與業(yè)務(wù)邏輯相關(guān)的代碼菊卷,其他的都是附屬的代碼缔恳。
package com.example.repository;
public class TraditionalPersonRepoImpl implements PersonRepo {
public List<String> getAllPersonNames() {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/dc=example,dc=com");
DirContext ctx;
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
throw new RuntimeException(e);
}
List<String> list = new LinkedList<String>();
NamingEnumeration results = null;
try {
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
results = ctx.search("", "(objectclass=person)", controls);
while (results.hasMore()) {
SearchResult searchResult = (SearchResult) results.next();
Attributes attributes = searchResult.getAttributes();
Attribute attr = attributes.get("cn");
String cn = attr.get().toString();
list.add(cn);
}
} catch (NameNotFoundException e) {
// The base context was not found.
// Just clean up and exit.
} catch (NamingException e) {
throw new RuntimeException(e);
} finally {
if (results != null) {
try {
results.close();
} catch (Exception e) {
// Never mind this.
}
}
if (ctx != null) {
try {
ctx.close();
} catch (Exception e) {
// Never mind this.
}
}
}
return list;
}
}
注:使用markdown編輯不知道怎么在代碼中加粗,求大佬告知洁闰,詳細(xì)加粗部分可參考:https://docs.spring.io/spring-ldap/docs/current/reference/#introduction
通過使用Spring LDAP的AttributesMapper類與LdapTemplate類歉甚,我們可以編寫下面的代碼實(shí)現(xiàn)相同的功能:
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public List<String> getAllPersonNames() {
return ldapTemplate.search(
query().where("objectclass").is("person"),
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return attrs.get("cn").get().toString();
}
});
}
}
上述代碼中公式化的代碼明顯比傳統(tǒng)方法少了許多。LdapTemplate的search方法能確保創(chuàng)建DirContext實(shí)例扑眉,執(zhí)行查詢纸泄,并使用給定 的AttributesMapper來將屬性attributes轉(zhuǎn)換成字符串String,然后將字符串放入list中腰素,最后返回這個(gè)list聘裁。它同時(shí)也能確保NamingEnumeration與DirContext能夠正確的關(guān)閉,并且處理可能出現(xiàn)的異常弓千。
不用說衡便,這是Spring框架的子項(xiàng)目,我們可以使用Spring來配置我們的應(yīng)用计呈。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ldap="http://www.springframework.org/schema/ldap"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/ldap http://www.springframework.org/schema/ldap/spring-ldap.xsd">
<ldap:context-source
url="ldap://localhost:389"
base="dc=example,dc=com"
username="cn=Manager"
password="secret" />
<ldap:ldap-template id="ldapTemplate" />
<bean id="personRepo" class="com.example.repo.PersonRepoImpl">
<property name="ldapTemplate" ref="ldapTemplate" />
</bean>
</beans>
為了能使用LDAP定制的XML命名空間namespace來配置Spring LDAP組件砰诵,你需要向上面的例子一樣,將該命名空間namespace的引用加入到自己的XML聲明中捌显。
1.3 2.2版本的新特性
2.2版本更加詳細(xì)完整的信息請(qǐng)參閱變更日志2.2.0.RC1茁彭。以下是Spring LDAP 2.2版本的亮點(diǎn):
1.4 2.1版本的新特性
2.1版本更加詳細(xì)完整的信息請(qǐng)參閱變更日志2.1.0.RC1與2.1.0。以下是2.1版本的亮點(diǎn):
- #390 - Spring Data Hopper的支持
- #351 - 支持commons-pool2
- #370 - 支持在XML 命名空間中書寫合適的占位符
- #392 - 支持Document Testing 文檔的測(cè)試
- #401 - 轉(zhuǎn)為使用 assertj
- 從JIRA遷移到GitHub Issues
- 增加 Gitter Chat
1.5 2.0版本的新特性
盡管2.0版本的Spring LDAP API有了很明顯的改變扶歪,但我們?nèi)匀粸橄蚝蠹嫒荼M我們最大的努力理肺。使用了Spring LDAP 1.3.X 版本的代碼升級(jí)為2.0的lib后無需修改即可編譯與運(yùn)行,基本不會(huì)有異常產(chǎn)生善镰。
為了幾個(gè)重要的代碼重構(gòu)妹萨,我們將一小部分類移動(dòng)到了新的包packages中,當(dāng)代碼中用到這些類的時(shí)候可能會(huì)報(bào)異常炫欺。被移動(dòng)的類通常不會(huì)是公共的被人們調(diào)用的API乎完,并且整個(gè)遷移過程非常順利。如果升級(jí)后無論哪個(gè)Spring LDAP 的類找不到了品洛,都可以在IDE開發(fā)工具中重新import導(dǎo)入树姨。由于有許多改進(jìn)后的API,你可能會(huì)遇到提醒你“過時(shí)”deprecation的警告桥状,我們推薦不再使用過時(shí)的類與方法帽揪,并盡可能多的使用2.0版本提供的新的以及改進(jìn)的API。
以下列表顯示了在Spring LDAP 2.0 中的重要更改:
- Spring LDAP要求至少JAVA 6版本辅斟。支持Spring 2及以上版本转晰。
- 核心API根據(jù)JAVA5+的特性(如泛型與可變參數(shù))進(jìn)行了升級(jí)。因此整個(gè)spring-ldap-tiger模塊都變的過時(shí)了deprecated,我們建議使用者轉(zhuǎn)而使用Spring LDAP的核心類查邢。在現(xiàn)有代碼中蔗崎,參數(shù)化的核心接口會(huì)導(dǎo)致一些編譯警告產(chǎn)生,使用這些API的用戶要采取適當(dāng)?shù)姆绞絹砣サ暨@些警告侠坎。
- ODM(Object-Directory Mapping)功能被移動(dòng)到core包中蚁趁。并且LdapOperations/LdapTemplate中的一些方法使用了ODM的功能,能夠?qū)傩宰詣?dòng)轉(zhuǎn)化為被ODM注解的類對(duì)象实胸,或從ODM注解的類對(duì)象獲取屬性值他嫡。更詳細(xì)的內(nèi)容請(qǐng)參考Object-Directory Mapping (ODM)
- 現(xiàn)在終于能夠通過XML命名空間來簡(jiǎn)化配置Spring LDAP了。詳細(xì)信息請(qǐng)參考Configuration
- Spring LDAP 提供了對(duì)Spring Data Repository 和 QueryDSL的參考庐完。詳情參閱Spring LDAP Repositories
- 在DirContextAdapter 與 ODM中钢属,作為屬性值的Name與DN(distinguished name)是等價(jià)的。詳情參閱DirContextAdapter and Distinguished Names as Attribute Values.與ODM and Distinguished Names as Attribute Values.
- DistinguishedName 以及相關(guān)的類已經(jīng)過時(shí)了门躯,并且被JAVA的LdapName所取代淆党。詳情參閱Dynamically Building Distinguished Names,該文章說明了如何使用函數(shù)庫中的LdapNames讶凉。
- 新增了支持更加流暢的創(chuàng)建LDAP的查詢query染乌。這讓我們能在Spring LDAP中使用LDAP查詢時(shí)有了更好的編程體驗(yàn)。關(guān)于創(chuàng)建LDAP查詢query的更多信息請(qǐng)參閱Building LDAP Queries與Advanced LDAP Queries
- LdapTemplate 的舊有的authenticate 已經(jīng)過時(shí)懂讯。被LdapQuery 對(duì)象中的幾個(gè)authenticate 新方法所取代荷憋,新方法能夠在認(rèn)證失敗后拋出異常,這讓用戶在查找認(rèn)證失敗的原因時(shí)變得簡(jiǎn)單褐望。
- 使用2.0版本特性寫的示例已經(jīng)上傳了勒庄。為了寫這個(gè)LDAP 用戶管理應(yīng)用的示例我們付出了很大的努力。
1.6 包的概覽
為了使用Spring LDAP你至少需要一下:
- spring-ldap-core (Spring LDAP 函數(shù)庫)
- spring-core(框架內(nèi)部使用的各種實(shí)用類)
- spring-beans (包含操作JAVA beans的接口和類)
- spring-data-commons(支持repository的基礎(chǔ)內(nèi)容等等)
- slf4j (內(nèi)部使用的簡(jiǎn)單的日志記錄)
除了上述必須的依賴外瘫里,下面這些依賴在使用一些特定功能時(shí)也是必須的: - spring-context (Spring Application Context能夠?yàn)槟愕膽?yīng)用程序?qū)ο笤黾邮褂媒y(tǒng)一API獲取資源的能力实蔽,如果你的應(yīng)用程序是使用Spring Application Context你可能需要這個(gè)依賴。如果你打算使用BaseLdapPathBeanPostProcessor谨读,你一定需要這個(gè)依賴)
- spring-tx(如果你打算使用客戶端補(bǔ)償事務(wù)的支持的話局装,需要該依賴)
- spring-jdbc(如果你打算使用客戶端補(bǔ)償事務(wù)的支持的話,需要該依賴)
- commons-pool (如果你打算使用連接池的功能劳殖,需要該依賴)
- spring-batch(如果你需要解析LDIF文件的功能贼邓,需要該依賴)
1.7 如何開始
這個(gè)示例提供了一些有用的示范,它們展示了一下常見的Spring LDAP的使用案例闷尿。
1.8 獲得支持
社區(qū)論壇地址:
http://forum.spring.io/forum/spring-projects/data/ldap。項(xiàng)目官網(wǎng):
http://spring.io/spring-ldap/
1.9 致謝
Spring LDAP項(xiàng)目初期由Jayway資助女坑。目前項(xiàng)目的維護(hù)是由Pivotal資助填具。
感謝Structure101(用于項(xiàng)目架構(gòu))提供的開源許可證。這幫助我們有一個(gè)良好的項(xiàng)目結(jié)構(gòu)。
2. 基本用法
2.1 在search 與 lookup中使用 AttributesMapper
在這個(gè)例子中劳景,我們將使用AttributesMapper
來輕松得到一個(gè)包含所有person對(duì)象的name屬性值的list列表誉简。
使用AttributesMapper 返回(獲取)單一屬性
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public List<String> getAllPersonNames() {
return ldapTemplate.search(
query().where("objectclass").is("person"),
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return (String) attrs.get("cn").get();
}
});
}
}
這個(gè)AttributesMapper 通過Attributes 來獲取需要的屬性值并返回這個(gè)值盟广。本質(zhì)上闷串,LdapTemplate 會(huì)循環(huán)所有找到的條目entry,對(duì)每個(gè)條目都調(diào)用AttributesMapper 并將結(jié)果放入list中筋量,最終這個(gè)list會(huì)作為search方法的返回值烹吵。
需要注意的是我們能夠很容易的修改AttributesMapper 的實(shí)現(xiàn)(implementation)來獲取整個(gè)person對(duì)象:
使用AttributesMapper 返回person對(duì)象
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
private class PersonAttributesMapper implements AttributesMapper<Person> {
public Person mapFromAttributes(Attributes attrs) throws NamingException {
Person person = new Person();
person.setFullName((String)attrs.get("cn").get());
person.setLastName((String)attrs.get("sn").get());
person.setDescription((String)attrs.get("description").get());
return person;
}
}
public List<Person> getAllPersons() {
return ldapTemplate.search(query()
.where("objectclass").is("person"), new PersonAttributesMapper());
}
}
LDAP中的條目是根據(jù)DN來唯一標(biāo)識(shí)。如果你有一個(gè)條目的DN值桨武,那你可以通過該值來直接獲取到該條目肋拔,這在java LDAP中被稱為lookup。下面的例子展示了如何使用lookup獲取person對(duì)象:
使用lookup獲取person對(duì)象
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public Person findPerson(String dn) {
return ldapTemplate.lookup(dn, new PersonAttributesMapper());
}
}
上面的程序?qū)?huì)找到特定的DN然后將屬性值傳遞給提供的AttributesMapper呀酸,該例中使用了PersonAttributesMapper凉蜂,將會(huì)返回一個(gè)person對(duì)象。
2.2. 創(chuàng)建 LDAP Queries
LDAP查詢search方法包括以下幾項(xiàng)參數(shù):
- Base LDAP path -從LDAP樹形結(jié)構(gòu)哪個(gè)幾點(diǎn)開始查詢
- Search scope -查詢范圍性誉,說明了LDAP樹形結(jié)構(gòu)的查詢深度
- Attributes 返回的屬性
- Search filter -使用scope來查詢時(shí)的查詢條件
為了更好的創(chuàng)建LDAP 查詢窿吩,Spring LDAP提供了LdapQueryBuilder
類以及一些好用的API 方法。
假設(shè)我們想要執(zhí)行一個(gè)查詢错览,它的base DN為:dc=261consulting,dc=com,返回sn與cn屬性值纫雁,查詢條件為(&(objectclass=person)(sn=?)),我們希望查詢條件中的?會(huì)被sn的值(lastName)所取代蝗砾,我們可以使用LdapQueryBuilder完成上面的查詢:
動(dòng)態(tài)創(chuàng)建search filter查詢條件
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public List<String> getPersonNamesByLastName(String lastName) {
LdapQuery query = query()
.base("dc=261consulting,dc=com")
.attributes("cn", "sn")
.where("objectclass").is("person")
.and("sn").is(lastName);
return ldapTemplate.search(query,
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return (String) attrs.get("cn").get();
}
});
}
}
除了簡(jiǎn)化創(chuàng)建一個(gè)復(fù)雜的查詢外先较,LdapQueryBuilder 以及與之相關(guān)的類同時(shí)也提供了針對(duì)查詢條件中的非安全字符的合適處理。這能夠防止"LDAP注入"悼粮,類比“sql注入”闲勺,“LDAP注入”是指用戶使用一些字符想LDAP中執(zhí)行一些我們不希望產(chǎn)生的操作。
在LdapTemplate 類中有許多重載的方法來執(zhí)行LDAP查詢操作扣猫。這是為了適應(yīng)盡可能多的不同的用戶情景與編程風(fēng)格菜循。在絕大多數(shù)的使用案例中,我們推薦使用將LdapQuery作為輸入?yún)?shù)的方法 申尤。
在執(zhí)行search 與 lookup查詢數(shù)據(jù)時(shí)癌幕,AttributesMapper 只是可用的回調(diào)接口中的其中一種∶链可以查閱 Simplifying Attribute Access and Manipulation with DirContextAdapter
使用其他可用的AttributesMapper 的替代類勺远。
欲知更多的關(guān)于LdapQueryBuilder 的信息,請(qǐng)參閱Advanced LDAP Queries
2.3. 動(dòng)態(tài)創(chuàng)建DN
DN的JAVA實(shí)現(xiàn) --LdapName时鸵,能夠在解析DN值時(shí)有不錯(cuò)的表現(xiàn)胶逢。但是在實(shí)際應(yīng)用中厅瞎,這個(gè)JAVA實(shí)現(xiàn)有如下一些缺點(diǎn):
- LdapName 的實(shí)現(xiàn)類是狀態(tài)可變的(讀者可以以java mutable為關(guān)鍵字搜索java可變狀態(tài)對(duì)象的相關(guān)信息),這一特點(diǎn)不太適合展示對(duì)象的唯一標(biāo)識(shí)初坠。
- 盡管它有狀態(tài)可變的特性和簸,但是使用LdapName來動(dòng)態(tài)創(chuàng)建或修改DN值寫起來也有些冗長(zhǎng)。因此使用其來提取某個(gè)索引的值或者(尤其是)根據(jù)名稱來取值會(huì)顯得有點(diǎn)尷尬碟刺。
- 許多LdapName的操作會(huì)拋出編譯期異常(checked Exception)锁保,這要求我們使用try-catch來包裹程序,然而這些error是程序錯(cuò)誤并且無法用有意義的方式進(jìn)行處理半沽。
為了簡(jiǎn)化針對(duì)DN的操作爽柒,Spring LDAP提供了LdapNameBuilder
以及LdapUtils
中的一系列相關(guān)方法,來操作并封裝LdapName抄囚。
2.3.1. 示例
*使用LdapNameBuilder來動(dòng)態(tài)創(chuàng)建一個(gè)LdapName *
package com.example.repo;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
public class PersonRepoImpl implements PersonRepo {
public static final String BASE_DN = "dc=example,dc=com";
protected Name buildDn(Person p) {
return LdapNameBuilder.newInstance(BASE_DN)
.add("c", p.getCountry())
.add("ou", p.getCompany())
.add("cn", p.getFullname())
.build();
}
...
假設(shè)Person對(duì)象有一下屬性
Attribute Name | Attribute Value |
---|---|
country | Sweden |
company | Some Company |
fullname | Some Person |
上面的代碼將會(huì)返回如下的DN:
cn=Some Person, ou=Some Company, c=Sweden, dc=example, dc=com
使用LdapUtils根據(jù)DN來提取屬性值
package com.example.repo;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
public class PersonRepoImpl implements PersonRepo {
...
protected Person buildPerson(Name dn, Attributes attrs) {
Person person = new Person();
person.setCountry(LdapUtils.getStringValue(dn, "c"));
person.setCompany(LdapUtils.getStringValue(dn, "ou"));
person.setFullname(LdapUtils.getStringValue(dn, "cn"));
// Populate rest of person object using attributes.
return person;
}
由于1.4一下的java版本沒有提供DN的實(shí)現(xiàn)類霉赡,Spring LDAP 1.x提供了它自己的實(shí)現(xiàn)類 DistinguishedName。這個(gè)實(shí)現(xiàn)自身有些缺點(diǎn)并且在2.0版本中被標(biāo)為了過時(shí)deprecated方法♂M校現(xiàn)在推薦用戶使用上面所示例的LdapName的封裝的一些工具類如LdapUtils來替代DistinguishedName穴亏。
2.4. 綁定與解綁(增加與刪除)
2.4.1. 增加數(shù)據(jù)
在Java LDAP中,增加數(shù)據(jù)被稱為綁定重挑。這有時(shí)候會(huì)造成困惑嗓化,因?yàn)樵贚DAP的術(shù)語中綁定一詞有完全不同的意義。JNDI中的綁定就是LDAP中的增加數(shù)據(jù)谬哀,將一個(gè)含有一些列屬性值的并有著唯一DN值的條目entry插入到ldap中刺覆。下面的示例中展示了如何使用LdapTemplate來插入數(shù)據(jù):
使用Attributes來插入數(shù)據(jù)
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void create(Person p) {
Name dn = buildDn(p);
ldapTemplate.bind(dn, null, buildAttributes(p));
}
private Attributes buildAttributes(Person p) {
Attributes attrs = new BasicAttributes();
BasicAttribute ocattr = new BasicAttribute("objectclass");
ocattr.add("top");
ocattr.add("person");
attrs.put(ocattr);
attrs.put("cn", "Some Person");
attrs.put("sn", "Person");
return attrs;
}
}
這樣手動(dòng)的添加屬性雖然很繁瑣并且冗長(zhǎng),但是能夠很好的滿足我們的要求史煎。當(dāng)然也能夠更加簡(jiǎn)化該增加數(shù)據(jù)的操作谦屑,請(qǐng)參閱:Simplifying Attribute Access and Manipulation with DirContextAdapter。
2.4.2 刪除數(shù)據(jù)
在Java LDAP中刪除數(shù)據(jù)被稱為解綁(unbinding)篇梭。JNDI中的解綁就是LDAP中的刪除操作氢橙,該操作會(huì)根據(jù)DN值把特定的條目從LDAP樹種刪除。下面的示例展示了如何使用LdapTemplate來刪除數(shù)據(jù):
刪除數(shù)據(jù)
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void delete(Person p) {
Name dn = buildDn(p);
ldapTemplate.unbind(dn);
}
}
2.5. 更新數(shù)據(jù)
在Java LDAP中恬偷,我們提供了兩種方式來修改數(shù)據(jù):rebind 與 modifyAttributes悍手。
2.5.1. 使用rebind更新數(shù)據(jù)
使用rebind來更新數(shù)據(jù)時(shí)最原始的方法,它就是簡(jiǎn)單的先刪除數(shù)據(jù)再添加數(shù)據(jù)袍患。下面的示例展示了rebind的使用:
使用rebind更新數(shù)據(jù)
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void update(Person p) {
Name dn = buildDn(p);
ldapTemplate.rebind(dn, null, buildAttributes(p));
}
}
2.5.2. 使用modifyAttributes更新數(shù)據(jù)
一個(gè)更好的更新數(shù)據(jù)的方法就是使用modifyAttributes坦康。這個(gè)方法會(huì)將要更新的屬性放入一個(gè)數(shù)組中,并在特定的條目中更新這些屬性的值:
使用modifyAttributes更新數(shù)據(jù)
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void updateDescription(Person p) {
Name dn = buildDn(p);
Attribute attr = new BasicAttribute("description", p.getDescription())
ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
ldapTemplate.modifyAttributes(dn, new ModificationItem[] {item});
}
}
譯者注:上述示例僅僅更新了person p對(duì)應(yīng)的dn這個(gè)條目中的description屬性诡延,讀者可以創(chuàng)建多個(gè)要更新的Attribute對(duì)象滞欠,放入數(shù)組中,如:new ModificationItem[] {item,item2}
創(chuàng)建Attributes 與 ModificationItem也是很大的工作量肆良,但是您可以參考Simplifying Attribute Access and Manipulation with DirContextAdapter仑撞,Spring LDAP提供了更多的內(nèi)容來簡(jiǎn)化這些操作赤兴。
3. 使用DirContextAdapter來簡(jiǎn)化Attribute屬性的獲與操作
3.1 引言
Java LDAP API中鮮為人知并且可能被輕視的一個(gè)特點(diǎn)就是能夠從找到的LDAP entries中注冊(cè)DirObjectFactory 類來自動(dòng)創(chuàng)建對(duì)象。Spring LDAP中某些search 與 lookup方法使用了這一特點(diǎn)并且返回一個(gè)DirContextAdapter
實(shí)例隧哮。
當(dāng)我們操作LDAP的屬性時(shí),DirContextAdapter 時(shí)一個(gè)非常有用的工具座舍,尤其是當(dāng)我們?cè)黾优c修改數(shù)據(jù)的時(shí)候沮翔。
3.2. 在search 與 lookup中使用ContextMapper
只要在LDAP樹種找到某個(gè)entry時(shí),Spring LDAP會(huì)根據(jù)該entry的屬性與DN來構(gòu)建一個(gè)DirContextAdapter曲秉。這能讓我們來用ContextMapper
來替代上文中使用的AttributesMapper 來修改找到的屬性值:
在查詢中使用ContextMapper
package com.example.repo;
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 ldapTemplate.lookup(dn, new PersonContextMapper());
}
}
就像上面展示的那樣采蚀,我們能直接通過屬性名獲取到屬性值,而不用像之前那樣使用Attributes類與Attribute類承二。這在操作多值屬性時(shí)非常有用榆鼠。獲取多值屬性的值時(shí),一般需要先得到Attributes 實(shí)現(xiàn)類的返回值亥鸠,然后循環(huán)NamingEnumeration 妆够。而通過DirContextAdapter 的
getStringAttributes()
或 getObjectAttributes()
方法可以輕松做到上述要求:
使用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;
}
}
3.2.1. AbstractContextMapper
Spring LDAP提供了一個(gè)ContextMapper的一個(gè)抽象實(shí)現(xiàn),AbstractContextMapper
负蚊。該類可以自動(dòng)將Object對(duì)象轉(zhuǎn)為DirContexOperations神妹。使用AbstractContextMapper后,上面的PersonContextMapper 可以改寫為如下示例:
使用AbstractContextMapper
private static class PersonContextMapper extends AbstractContextMapper {
public Object doMapFromContext(DirContextOperations ctx) {
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(context.getStringAttribute("description"));
return p;
}
}
譯者注:我認(rèn)為官網(wǎng)文檔這里寫錯(cuò)了家妆,應(yīng)該將上面的context改為ctx鸵荠。讀者對(duì)比該示例與之前的PersonContextMapper 的示例,可以發(fā)現(xiàn)AbstractContextMapper 的作用就是將doMapFromContext中的入?yún)腛bject ctx自動(dòng)轉(zhuǎn)換為DirContextOperations ctx伤极,這樣在代碼中就省去了如下的強(qiáng)制轉(zhuǎn)換:
DirContextAdapter context = (DirContextAdapter)ctx
猜測(cè)官網(wǎng)文檔作者只是簡(jiǎn)單的復(fù)制粘貼蛹找,忘記了修改變量名。
3.3. 使用DirContextAdapter來增加與更新數(shù)據(jù)
盡管在獲取屬性值方面DirContextAdapter非常有用哨坪,在管理LDAP其他方面包括增加與更新數(shù)據(jù)中DirContextAdapter表現(xiàn)的更加搶眼庸疾。
3.3.1. 增加數(shù)據(jù)
針對(duì)上文Adding Data中的create方法,我們使用DirContextAdapter 可以更好的改良create方法齿税,示例如下:
使用DirContextAdapter 增加數(shù)據(jù)
package com.example.repo;
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());
ldapTemplate.bind(context);
}
}
注意我們使用DirContextAdapter 實(shí)例作為bind的第二個(gè)參數(shù)(彼硫?泳挥?陨亡?),這應(yīng)該是一個(gè)Context螺垢。因?yàn)槲覀儧]有指定某個(gè)屬性牵舱,因此第三個(gè)參數(shù)為null串绩。
譯者注:這里我查閱了ldapTemplate.bind()方法,如下:
bind(DirContextOperations ctx)
bind(Name dn, Object obj,Attributes attributes)
而DirContextAdapter 是DirContextOperations的實(shí)現(xiàn)類芜壁,所以這里使用DirContextAdapter 作為第一參數(shù)套用的是bind(DirContextOperations ctx)方法礁凡,不存在三個(gè)參數(shù)的問題高氮。我猜測(cè)這里所謂的第二參數(shù)是從bind方法的源碼實(shí)現(xiàn)角度來講的,bind(DirContextOperations ctx)方法在源碼中應(yīng)該是使用的bind(Name dn ,Object obj,Attributes attributes)方法顷牌。
同時(shí)注意剪芍,在對(duì)objectclass 屬性賦值時(shí),我們使用了setAttributeValues()方法窟蓝。objectclass 屬性時(shí)多值屬性罪裹,于獲取多值屬性的值時(shí)的問題相同,普通方法來設(shè)置多值屬性的值會(huì)顯得枯燥和冗長(zhǎng)运挫。我們可以使用DirContextAdapter 的setAttributeValues()來完成該操作状共。
3.3.2. 更新數(shù)據(jù)
我們之間看到更新數(shù)據(jù)建議的方法是使用modifyAttributes ,但這個(gè)方法要求我們事先知道哪些屬性被修改了谁帕,并根據(jù)這些要更新的屬性創(chuàng)建ModificationItem 數(shù)組峡继。DirContextAdapter 能幫我們做以上的事情。
使用DirContextAdapter更新數(shù)據(jù)
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public void update(Person p) {
Name dn = buildDn(p);
DirContextOperations context = ldapTemplate.lookupContext(dn);
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description", p.getDescription());
ldapTemplate.modifyAttributes(context);
}
}
通過上面的代碼可以看到在代碼中使用的是DirContextOperations 匈挖,這是因?yàn)椋寒?dāng)沒有參數(shù)傳入ldapTemplate.lookup()時(shí)(no mapper is passed to a ldapTemplate.lookup())碾牌,返回的是DirContextAdapter 對(duì)象,但當(dāng)查詢方法返回一個(gè)Object對(duì)象時(shí)关划,lookupContext 方法會(huì)很方便的將返回值轉(zhuǎn)化為DirContextOperations (這是DirContextAdapter 實(shí)現(xiàn)的一個(gè)接口)
譯者注:我的理解是當(dāng)我們create一個(gè)entry時(shí)小染,根據(jù)dn創(chuàng)建即可,沒有用到查詢方法贮折,所用使用的是DirContextAdapter
細(xì)心的讀者可以已經(jīng)發(fā)現(xiàn)我們?cè)赾reate與update方法中有很多重復(fù)的代碼裤翩。這些代碼講一個(gè)對(duì)象映射到上下文context上。這部分能夠提取出來作為一個(gè)單獨(dú)的方法:
使用DirContextAdapter增加與更新數(shù)據(jù)
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);
context.setAttributeValues("objectclass", new String[] {"top", "person"});
mapToContext(p, context);
ldapTemplate.bind(context);
}
public void update(Person p) {
Name dn = buildDn(p);
DirContextOperations context = ldapTemplate.lookupContext(dn);
mapToContext(person, context);
ldapTemplate.modifyAttributes(context);
}
protected void mapToContext (Person p, DirContextOperations context) {
context.setAttributeValue("cn", p.getFullName());
context.setAttributeValue("sn", p.getLastName());
context.setAttributeValue("description", p.getDescription());
}
}
3.4. DirContextAdapter 與作為屬性值的DN值
在LDAP中管理安全組時(shí)调榄,將DN值作為屬性值是很常見的事情踊赠。由于判斷DN相等于判斷字符串相等有很大的區(qū)別(比如在判斷DN相等時(shí)是忽略空格和大小寫的),因此在判斷屬性是否發(fā)生變化需要更新時(shí)每庆,簡(jiǎn)單的使用字符串是否相等來判斷可能不會(huì)達(dá)到我們預(yù)期的效果筐带。
比如:member屬性的value值為:cn=John Doe,ou=People,并且我們?cè)诖a中寫為:ctx.addAttributeValue("member", "CN=John Doe, OU=People")缤灵,這個(gè)屬性就會(huì)為認(rèn)為有兩個(gè)值伦籍,盡管這兩個(gè)字符串代表的是同一個(gè)DN值。
自Spring LDAP 2.0起腮出,我們針對(duì)屬性更新的方法提供了javax.naming.Name對(duì)象帖鸦,當(dāng)使用更新方法時(shí),會(huì)讓DirContextAdapter 使用DN是否相等來判斷屬性值是否發(fā)生變化胚嘲。如果我們將上面的例子修改如下:
ctx.addAttributeValue("member",LdapUtils.newLdapName("CN=John Doe, OU=People"))
就不會(huì)觸發(fā)更新操作作儿。
*使用DirContextAdapter更新Group中的成員Membership *
public class GroupRepo implements BaseLdapNameAware {
private LdapTemplate ldapTemplate;
private LdapName baseLdapPath;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
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 = ldapTemplate.lookupContext(groupDn);
ctx.addAttributeValue("member", userDn);
ldapTemplate.update(ctx);
}
public void removeMemberFromGroup(String groupName, Person p) {
Name groupDn = buildGroupDn(String groupName);
Name userDn = buildPersonDn(
person.getFullname(),
person.getCompany(),
person.getCountry());
DirContextOperation ctx = ldapTemplate.lookupContext(groupDn);
ctx.removeAttributeValue("member", userDn);
ldapTemplate.update(ctx);
}
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();
}
}
在上面的例子中,我們實(shí)現(xiàn)了BaseLdapNameAware接口馋劈,該接口能讓我們獲取base LDAP路徑攻锰,更多的描述在 Obtaining a reference to the base LDAP path晾嘶。我們之所以要獲取base LDAP路徑是因?yàn)樽鳛閷傩灾荡鎯?chǔ)的DN必須是一個(gè)從根節(jié)點(diǎn)開始的絕對(duì)路徑。
3.5. 一個(gè)完整的PersonRepository類
為了說明Spring LDAP 與 DirContextAdapter 的易用性娶吞,我們寫了下面的一個(gè)完整的PersonRepository類來操作LDAP:
package com.example.repo;
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 LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public void create(Person person) {
DirContextAdapter context = new DirContextAdapter(buildDn(person));
mapToContext(person, context);
ldapTemplate.bind(context);
}
public void update(Person person) {
Name dn = buildDn(person);
DirContextOperations context = ldapTemplate.lookupContext(dn);
mapToContext(person, context);
ldapTemplate.modifyAttributes(context);
}
public void delete(Person person) {
ldapTemplate.unbind(buildDn(person));
}
public Person findByPrimaryKey(String name, String company, String country) {
Name dn = buildDn(name, company, country);
return ldapTemplate.lookup(dn, getContextMapper());
}
public List findByName(String name) {
LdapQuery query = query()
.where("objectclass").is("person")
.and("cn").whitespaceWildcardsLike("name");
return ldapTemplate.search(query, getContextMapper());
}
public List findAll() {
EqualsFilter filter = new EqualsFilter("objectclass", "person");
return ldapTemplate.search(LdapUtils.emptyPath(), filter.encode(), 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;
}
}
}
在某些情況下一個(gè)對(duì)象的DN值是使用對(duì)象的屬性構(gòu)造的浸颓。例如晋涣,在上面的例子中佃扼,Person的DN就用到了Person中的country, company 以及full name屬性瘤运。這也就意味著我們?cè)诟耬ntry的屬性值的時(shí)候掠哥,同時(shí)也會(huì)使用rename()方法在LDAP樹中移動(dòng)entry。由于這是一個(gè)極其具體的實(shí)現(xiàn)(需求),就需要你自己去跟蹤上述過程侯嘀,要么你就不允許用戶更改person的屬性诗茎,要么你在用戶通過update()方法更改person屬性后執(zhí)行rename()方法玉掸。注意:如果你用了Object-Directory Mapping (ODM)饮睬,這個(gè)函數(shù)庫會(huì)自動(dòng)幫你處理這些事情呻逆,但你需要在你的相關(guān)類中加上相應(yīng)的注解辐董。
4. 對(duì)象-目錄Mapping(ODM)
4.1. 前言
基于對(duì)象-關(guān)系的映射框架揭绑,如Hibernate 以及JPA ,這些框架能夠讓開發(fā)者通過使用注解的方式在Java對(duì)象和關(guān)系型數(shù)據(jù)庫的表格之間建立映射郎哭。Spring LDAP項(xiàng)目也針對(duì)LDAP目錄通過LdapOperations類中的一些方法提供了類似的功能:
- <T> T findByDn(Name dn, Class<T> clazz)
- <T> T findOne(LdapQuery query, Class<T> clazz)
- <T> List<T> find(LdapQuery query, Class<T> clazz)
- <T> List<T> findAll(Class<T> clazz)
- <T> List<T> findAll(Name base, SearchControls searchControls, Class<T> clazz)
- <T> List<T> findAll(Name base, Filter filter, SearchControls searchControls, Class<T> clazz)
- void create(Object entry)
- void update(Object entry)
- void delete(Object entry)
4.2. 注解
需要對(duì)用于對(duì)象映射方法的實(shí)體類進(jìn)行注解,這些注解都來自與org.springframework.ldap.odm.annotations包菇存。該包中可選的注解都有下面這些:
- @Entry - 類級(jí)別的注解夸研,表明了該實(shí)體映射了那些objectClass。(必需)
- @Id - 標(biāo)識(shí)實(shí)體的DN依鸥;聲明此屬性的字段必須是javax.naming.Name及其子類亥至。(必需)
- @Attribute - 表明該類中的成員變量與那個(gè)屬性相互映射。
- @DnAttribute - 表明該類中的此成員變量與DN屬性相互映射贱迟。
- @Transient - 表示該成員變量不是永久變量姐扮,使用此注解,會(huì)讓OdmManager在映射時(shí)忽略此成員變量衣吠。
@Entry與@Id注解必需寫到托管類(映射的類)中茶敏。@Entry用來標(biāo)識(shí)該實(shí)體類映射哪些objectClass 以及該實(shí)體類映射的LDAP entry的根目錄(可選)。所有字段被映射的objectClass都必須聲明缚俏。(譯者注:我的理解是如果我們使用到了cn與sn屬性惊搏,那就必須要聲明person這個(gè)objectClass。注意不要與java代碼中的person混淆
)需要注意的是在根據(jù)托管類(映射類)創(chuàng)建LDAP 的 entry時(shí)忧换,只有被聲明的objectClass會(huì)被創(chuàng)建恬惯。
為了將目錄entry與托管類建立映射,目錄entry中聲明的objectClass都必須在@Entry注解中聲明亚茬。例如:假設(shè)在你的LDAP樹種有這么一個(gè)entry酪耳,它的objectClass包含:inetOrgPerson,organizationalPerson,person,top。如果你只想改變person這個(gè)objectClass中的屬性刹缝,你的@Entry應(yīng)該這樣寫:@Entry(objectClasses = { "person", "top" })
碗暗。然而颈将,如果你想管理inetOrgPerson 這個(gè)objectClass中屬性,你就要用到上面所有的objectClass了:@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" })
(譯者注:理解這句話需要LDAP的objectClass的相關(guān)知識(shí)
)讹堤。
@Id注解用來將entry的DN值映射到一個(gè)成員變量上吆鹤。這個(gè)成員變量必須是javax.naming.Name的實(shí)現(xiàn)類。
@Attribute注解用來實(shí)現(xiàn)objectClass中的屬性與實(shí)體類中的成員變量之間的映射洲守。為了保證精確的匹配疑务,@Attribute要求必須在映射的成員變量上聲明所映射的objectClass屬性名,而聲明LDAP屬性的OID的語法是非必須項(xiàng)梗醇。@Attribute也提供了類型聲明知允,該聲明可以讓你來表明該屬性可被LDAP JNDI視為基于二進(jìn)制的還是基于字符串的屬性。
@DnAttribute可以實(shí)現(xiàn)成員變量與entry的DN中的某部分相互映射叙谨。當(dāng)從目錄樹中讀取一個(gè)entry時(shí)温鸽,@DnAttribute會(huì)自動(dòng)將被注解的成員變量用DN中的某個(gè)值來正確填充。如果實(shí)體類中所有的@DnAttribute中的index都被指定了手负,那么在增加或更新數(shù)據(jù)的時(shí)候DN會(huì)自動(dòng)重新計(jì)算(譯者注:還記得上文中說的person中的屬性名作為DN的一部分的時(shí)候涤垫,在更新數(shù)據(jù)時(shí),需要rename() entry以便根據(jù)更新后的數(shù)據(jù)將entry移動(dòng)到合適的樹節(jié)點(diǎn)嘛竟终?這里使用ODM自動(dòng)完成了
)蝠猬。在更新數(shù)據(jù)的場(chǎng)景中,如果實(shí)體類的屬性時(shí)DN值的一部分统捶,這樣就能夠?qū)崿F(xiàn)自動(dòng)移動(dòng)entry到目錄樹的合適位置榆芦。
@Transient注解用戶表明該成員變量應(yīng)該被ODM所忽略,不匹配LDAP中的屬性喘鸟。需要注意的是匆绣,如果@DnAttribute沒有被綁定到屬性上,或者說被@DnAttribute所注解的成員變量只是作為DN的一部分什黑,但是在LDAP中沒有這個(gè)屬性崎淳,那它必須也要使用@Transient這個(gè)注解。
譯者注:如果還不好理解愕把,可以看下面的例子中的company成員變量
4.3. 執(zhí)行
當(dāng)所有的成員都被正確的配置與注解之后凯力,LdapTemplae的對(duì)象映射方法用法如下:
執(zhí)行
@Entry(objectClasses = { "person", "top" }, base="ou=someOu")
public class Person {
@Id
private Name dn;
@Attribute(name="cn")
@DnAttribute(value="cn", index=1)
private String fullName;
// No @Attribute annotation means this will be bound to the LDAP attribute
// with the same value
private String description;
@DnAttribute(value="ou", index=0)
@Transient
private String company;
@Transient
private String someUnmappedField;
// ...more attributes below
}
public class OdmPersonRepo {
@Autowired
private LdapTemplate ldapTemplate;
public Person create(Person person) {
ldapTemplate.create(person);
return person;
}
public Person findByUid(String uid) {
return ldapTemplate.findOne(query().where("uid").is(uid), Person.class);
}
public void update(Person person) {
ldapTemplate.update(person);
}
public void delete(Person person) {
ldapTemplate.delete(person);
}
public List<Person> findAll() {
return ldapTemplate.findAll(Person.class);
}
public List<Person> findByLastName(String lastName) {
return ldapTemplate.find(query().where("sn").is(lastName), Person.class);
}
}
譯者注:看了上面例子中的company是否理解了@Transient中的最后一句話?company在LDAP中并不存在這個(gè)屬性礼华,但我們?cè)贘AVA中創(chuàng)建的實(shí)體類為了業(yè)務(wù)需要而加入了這個(gè)成員變量咐鹤,company可以與DN中的ou的值進(jìn)行映射,如:
cn=abc,ou=someOu,dc=cmiot,dc=com
ODM會(huì)自動(dòng)將ou的值someOu賦值給company圣絮,但是通過Person實(shí)體類創(chuàng)建一個(gè)entry時(shí)祈惶,并不會(huì)將company的值寫入LDAP,因?yàn)長(zhǎng)DAP中并無該屬性,且Person的@Entry注解已經(jīng)說明了新增的entry的DN中的ou=someOu捧请。
4.4. ODM與作為屬性值的DN
LDAP中的安全組通常包含一個(gè)多值屬性凡涩,每一個(gè)屬性值都是系統(tǒng)中一個(gè)用戶的DN值。操作這種屬性時(shí)的困難點(diǎn)我們已經(jīng)在
DirContextAdapter and Distinguished Names as Attribute Values.中將過了疹蛉。
ODM同樣支持javax.naming.Name作為屬性值活箕,這讓群組的更新變得非常簡(jiǎn)單:
群組的例子
@Entry(objectClasses = {"top", "groupOfUniqueNames"}, base = "cn=groups")
public class Group {
@Id
private Name dn;
@Attribute(name="cn")
@DnAttribute("cn")
private String name;
@Attribute(name="uniqueMember")
private Set<Name> members;
public Name getDn() {
return dn;
}
public void setDn(Name dn) {
this.dn = dn;
}
public Set<Name> getMembers() {
return members;
}
public void setMembers(Set<Name> members) {
this.members = members;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void addMember(Name member) {
members.add(member);
}
public void removeMember(Name member) {
members.remove(member);
}
}
譯者注:注意上面的類中除了get/set方法,還有addMember與removeMember方法可款。其中members是一個(gè)set育韩,存放的是Name類型的數(shù)據(jù),也就是DN闺鲸。
在更新群組信息時(shí)筋讨,只需實(shí)例化該群組對(duì)象,然后根據(jù)需要調(diào)用setMembers, addMember 以及 removeMember方法摸恍,并調(diào)用ldapTemplate.update()即可悉罕,該過程會(huì)根據(jù)DN值來判斷屬性是否有更新,這就意味著在判斷DN是否相等時(shí)會(huì)忽略文本格式立镶。
5. 高級(jí) LDAP 查詢
5.1. LDAP Query Builder參數(shù)
LdapQueryBuilder 及其相關(guān)類意在提供能夠用于LDAP查詢的所有參數(shù)壁袄。它提供了下面的參數(shù):
- base - 指定LDAP樹的根節(jié)點(diǎn)DN,并從此節(jié)點(diǎn)開始查詢
- searchScope - 指定在LDAP樹中媚媒,查詢的深度
- attributes - 指定查詢后返回的屬性然想,默認(rèn)為全部
- countLimit - 指定查詢返回的最大條目數(shù)
- timeLimit - 指定查詢所花費(fèi)的最長(zhǎng)時(shí)間
- Search filter - 查詢的條目必須滿足的條件
當(dāng)我們調(diào)用LdapQueryBuilder的query 方法時(shí),LdapQueryBuilder就已經(jīng)被創(chuàng)建了欣范。LdapQueryBuilder意在成為一個(gè)好用的構(gòu)建工具類API,它先定義基礎(chǔ)的參數(shù)令哟,然后才是過濾器的參數(shù)恼琼。一旦調(diào)用LdapQueryBuilder的where 方法定義了過濾器的參數(shù)后,再去試圖調(diào)用如base等方法會(huì)被拒絕屏富。查詢的基本參數(shù)是可選的晴竞,但是過濾器方法應(yīng)該至少被調(diào)用一次。
查詢所有的objectClass為person的entry
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().where("objectclass").is("person"),
new PersonAttributesMapper());
查詢所有objectClass為person且cn=John Doe的entry
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().where("objectclass").is("person")
.and("cn").is("John Doe"),
new PersonAttributesMapper());
由dc=261consulting,dc=com此節(jié)點(diǎn)開始查詢所有objectClass為person的所有entry狠半,并只返回cn屬性
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().base("dc=261consulting,dc=com")
.attributes("cn")
.where("objectclass").is("person"),
new PersonAttributesMapper());
使用or來查詢
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().where("objectclass").is("person"),
.and(query().where("cn").is("Doe").or("cn").is("Doo));
new PersonAttributesMapper());
5.2. 過濾器中的邏輯操作符
上面的例子中顯示了在LDAP過濾可以處理相等的情況噩死。LDAP 查詢同樣支持一下邏輯操作符:
- is -表示相等
- gte - 表示大于等于
- lte - 表示小于等于
- like - 表示模糊匹配,查詢中可以包含通配符神年,例如:where("cn").like("J*hn Doe")就相當(dāng)于根據(jù)條件(cn=J*hn Doe)來過濾
- whitespaceWildcardsLike - 所有的空格被通配符替代已维,例如:where("cn").whitespaceWildcardsLike("John Doe")就相當(dāng)于(cn=John*Doe)
- isPresent - 用于檢查屬性是否存在,如:where("cn").isPresent()
- not - 用于將過濾條件取反已日,例如:where("sn").not().is("Doe)就相當(dāng)于(!(sn=Doe))
5.3. 硬編碼的過濾器
如果你想要使用自定義的過濾器作為L(zhǎng)dapQuery的輸入也是可以的垛耳。LdapQueryBuilder 提供了兩種方式來做到這一點(diǎn):(譯者注:LdapQueryBuilder 是LdapQuery接口的實(shí)現(xiàn)類
)
- filter(String hardcodedFilter) - 使用特定的字符串作為過濾器。需要注意的是:輸入的特定字符串不會(huì)有任何驗(yàn)證,這就意味著如果你是為用戶輸入而創(chuàng)建這個(gè)自定義過濾器的話堂鲜,這個(gè)方法可能不適合
譯者注:這里我沒有很理解栈雳,查了doc文檔原文為:Never use direct user input and use it concatenating strings to use as LDAP filters. Doing so opens up for "LDAP injection", where malicious user may inject specifically constructed data to form filters at their convenience. When user input is used consider using
where(String)
orfilter(String, Object...)
instead
大意為:建議我們不要將用戶輸入以及將用戶輸入連接字符串作為L(zhǎng)DAP的過濾器。這樣做會(huì)有“LDAP 注入”的風(fēng)險(xiǎn)缔莲。當(dāng)你需要這么做到時(shí)候哥纫,可以考慮
where(String)
或filter(String, Object...)
作為替代方案
- filter(String filterFormat, String…? params) - 將特定字符串作為MessageFormat的輸入,將參數(shù)進(jìn)行編碼并在過濾條件語句的合適位置插入痴奏。
你不能同時(shí)使用硬編碼的過濾器與上文介紹的where過濾器蛀骇,這兩者只能選用其一。這也就意味著抛虫,如果你自定義了一個(gè)過濾器寫為filter()
松靡,當(dāng)你再調(diào)用where
時(shí)會(huì)產(chǎn)生異常。