本节提供LDAP和JNDI之间映射的详细信息。同时提供使用JNDI访问LDAP服务器的暗示和提示。
X.500,一个CCITT的目录服务器标准,是OSI否为套件的一部分。X.500标准定义了客户端应用程序访问X.500目录的协议,叫做目录访问协议(DAP)。它在开放系统互连(OSI)协议栈的顶层。
因特网委员会认为需要X.500类型的访问但底层的网络基础架构(是TCP/IP而不是OSI)不同,基于X.500 DAP协议设计了一个新协议,叫做轻量级DAP或LDAP。RFC 2251定义了成为版本3的LDAP(LDAPv3),这是它的前身LDAP v2(RFC 1777)的改进。
LDAP协议的目的是容易实现,特别是创建小的简单的客户端。一种尝试简化的成果是大量使用字符串来减少结构的使用。例如DN,在协议中以字符串表示,属性类型名和大多数属性值也是如此。
这个协议包含客户端向服务器发送的请求,对于服务器的应答,不需要按照请求的顺序进行。每一个请求有一个ID,所以请求和应答可以匹配。这个协议可以工作在TCP或UDP中,最常用的是TCP。
因为焦点在客户端,LDAP组织同时定义了DN的字符串表达(RFC 2553),搜索过滤器(RFC 1960),属性语法(RFC 1778),为C语言提供的API(RFC 1823),访问LDAP访问的URL格式(RFC 1959)。
LDAP v3支持国际化,多种认证技术,referral,以及一般的部署技术。使用extensions和controls添加新特性时不需要修改协议。
国际化是通过国际化字符集(ISO 10646)表示协议中的字符串元素(例如DN)。v3与v2不同,v2使用UTF-8编码字符串。
除了匿名,简单(明文密码)认证,LDAPv3使用简单认证以及安全层(SASL)认证架构(RFC 2222)允许在LDAP中使用不同的认证技术。SASL定义了客户端和服务器之间数据交换的认证的挑战-应答协议。
现在定义了一些SASL技术:DIGEST-MD5, CRAM-MD5,匿名,扩展,S/Key, GSSAPI以及Kerberos v4。LDAP v3客户端可以使用任意一种SASL技术,提供给支持这种技术的LDAP v3服务器。而且,新的(还没有定义的)SASL技术可以在不改变LDAP的情况下使用。
referral是服务器发送个客户端的信息,表示请求的信息可以在其他地方发现(很可能是其他服务器)。在LDAP v2中,服务器由服务器处理referral,不返回到客户端。因为实现referral是非常负载的并且可能导致很复杂的客户端。当服务器构建以及部署后,referral就变得十分有用,但不是很多服务器支持服务端的referral处理。所以,使用一种方法修改协议允许返回referral。通过在将referral放置在“partial result”错误应答的错误信息中实现。
LDAP v3显式支持referrals,允许服务器直接向客户端返回referrals。referrals不再本课中介绍,您可以求助于JNDI教程关于符合在应用程序中控制referrals的信息。
LDAP这种普通协议可以用来确保目录客户端和服务器“说同一种语言”。当不同的目录客户端和服务器部署在网络中时,这些实体都说同样的对象是非常有用的。
目录架构描述,在其他东西中间,对象的类型,目录可能有的对象的类型,对象的每个必须的或可选属性。LDAP v3定义架构(RFC 2252 和 RFC 2256)基于X.500标准为了在网络中查找基本对象定义,例如,国家,地区,组织,用户/人,用户组和设备。它定义了客户端访问服务器架构的方法,所以可以发现对象的类型以及服务器支持的属性。
LDAPv3进一步定义了表示属性值的一组语法(RFC 2252)。要编写访问架构信息的Java应用程序,请参考JNDI教程。
除了预定义的所有操作,例如“search”和“modify”之外,LDAPv3定义了“extended”操作。“extended”将请求当作参数并且返回应答。请求中包含标识请求的标识符和请求参数。应答中包含请求执行的结果。“extended”操作中的请求和应答叫做扩展。例如,可以为开始TLS定义扩展,它是客户端向服务器的请求,用来开始Start TLS协议。
扩展可以是标准的(LDAP委员会定义的)或者私有的(由供应商定义)。要编写使用扩展的程序请参考JNDI教程。
添加新特性的另一种方法是使用control。LDAP v3允许通过使用control修改所有操作的行为。一个操作中可以发送任意多个control,同时结果中也可以返回任意数量的control。例如,您可以和“search”操作一起发送排序control,告诉服务器根据“name”属性对结果进行。
和扩展一样,control可以是标准的也可以是私有的。标准control由平台提供。编写使用control的应用程序请参考JNDI教程。
JNDI和LDAP的模型在命名对象时都使用层次结构的名字空间。名字空间中的每一个对象都可能有属性,而且这些属性可以用来搜索对象。在这种层次上,两个模型是类似的,所以JNDI可以对LDAP进行很好的映射也没有什么稀奇的。
您可以将LDAP的条目想象成JNDI的DirContext。每个LDAP条目包含名称和一组属性,以及可选的子条目集合。例如,LDAP条目“o=JNDITutorial”可能有属性“objectclass”和“o”,同时可能有子条目“ou=Groups”和“ou=People”。
在JNDI中,LDAP条目“o=JNDITutorial”可以表示成名为“o=JNDITutorial”的上下文,它有两个子上下文,名叫“ou=Groups”和“ou=People”。LDAP条目属性使用Attributes接口表示,而独立的属性通过Attribute接口表示。
关于LDAP操作如何通过JNDI进行访问的详细信息请参考下一部分。
为了联合查询,您提供给JNDI上下文方法的名称可以跨越多个命名空间。这些叫做混合名字。当使用JNDI访问LDAP服务时,您需要明白字符串中的斜杠(“/”)在JNDI中特殊的含义。如果JNDI条目包含这个字符,需要进行转义(使用“\”)。例如,使用名称“cn=O/R”的LDAP条目在JNDI context的方法中,必须使用“cn=O\\/R”标识。关于名字的更多信息,请参考JNDI教程。LdapName和Rdn类简要介绍了创建和操作LDAP名字的方法。
协议中LDAP的名称总是完全的名字,表示从LDAP命名空间根开始的唯一条目(由服务器定义)。以下是一些LDAP全名的例子:
cn=John Smith, ou=Marketing, o=Some Corporation, c=gb cn=Vinnie Ryan, ou=People, o=JNDITutorial |
然而,在JNDI中,名字是相对的,即,您总是相对于上下文命名对象。例如,您可以相对于上下文“ou=People, o=JNDITutorial”对“cn=Vinnie Ryan”条目进行命名。或者,您可以相对于上下文“o=JNDITutorial”对条目“cn=Vinnie Ryan, ou=People”进行命名。或者,您可以创建初始上下文指向LDAP服务器命名空间的根,然后命名条目“cn=Vinnie Ryan, ou=People, o=JNDITutorial”。
在JNDI中,您同样可以使用LDAP的URL来命名LDAP条目。请参考JNDI教程中的LDAP URL讨论。
LDAP定义了一组操作或请求(RFC 2251)。在JNDI中,这些操作被映射到DirContext和LdapContext接口中,它们都是Context的子接口。例如,当请求DirContext中的方法时。LDAP服务提供者通过将LDAP请求发送给LDAP服务器实现这个操作。
下表描述和LDAP操作对应的JNDI方法:
LDAP操作 |
对应的JNDI方法 |
bind |
这是创建LDAP服务器初始连接的方式,对应JNDI中创建InitialDirContext对象。当应用程序创建初始上下文,它在环境参数中向服务器提供客户端认证信息。要修改一个已经存在上下文的认证信息,使用Context.addToEnvironment()和Context.removeFromEnvironment()。 |
Unbind |
Context.close()用来释放上下文使用的资源。服务提供者的实现和LDAP的unbind操作有一些不同,资源在上下文之间共享,所以关闭上下文当资源被其他上下文使用时就不会释放。如果您的意图是释放所有资源,需要关闭所有上下文。 |
Search |
JNDI中对应方法是DirContext.search()中接收搜索过滤器(RFC 2254)的重载形式。 |
modify |
JNDI中对应方法是DirContext.modifyAttributes()中接收DirContext.ModificationItems数组的重载形式。示例请看修改属性一节。 |
Add |
JNDI中对应方法是DirContext.bind()和DirContext.createSubcontext()。您可以使用它们添加一个新的LDAP条目。使用bind(),您不但需要指定新条目的属性集合同时需要和属性一起的Java对象。示例请参考关联属性的添加、替换绑定一节。 |
Delete |
JNDI中对应的方法是Context.unbind()和Context.destroySubcontext()。您可以使用它们移除LDAP条目。 |
modify DN/RDN |
JNDI中对应方法是Context.rename()。请参考重命名对象一节得到详细信息。 |
compare |
在JNDI中可以使用DirContext.search()代替。示例请参考LDAP比较操作一节。 |
abandon |
当您关闭上下文时,所有没有应答的请求都被放弃。类似的,当关闭NamingEnumeration,相应的LDAP“search”请求也放弃了。 |
Extended操作 |
JNDI中对应的方法是LdapContext.extendedOperation()。详细信息请参考JNDI教程。 |
LDAP定义了一组状态码,它们是由LDAP服务器作为应答发送给客户端的(RFC 2251)。在JNDI中,错误条件由Naming Exceptions子类的检查的异常标识。请参考JNDI异常类概述中的Naming Exceptions。
LDAP状态码 |
含义 |
异常或操作 |
0 |
成功 |
报告成功 |
1 |
操作错误 |
NamingException |
2 |
协议错误 |
CommunicationException |
3 |
达到时间限制 |
TimeLimitExceededException |
4 |
达到大小限制 |
SizeLimitExceededException |
5 |
比较失败 |
被DirContext.search()方法使用,不产生异常。 |
6 |
比较成功 |
被DirContext.search()方法使用,不产生异常。 |
7 |
认证方式不支持 |
AuthenticationNotSupportedException |
8 |
需要更强的认证 |
AuthenticationNotSupportedException |
9 |
只返回部分数据 |
如果环境参数“java.naming.referral”是“ignore”或错误的内容不包含referral,抛出PartialResultException。否则,使用内容创建一个referral。 |
10 |
发生referral |
如果环境参数“java.naming.referral”是“ignore”,则忽略。如果参数是“throw”,抛出ReferralException。如果属性是“follow”,由LDAP提供者处理referral。如果超过“java.naming.ldap.referral.limit”限制,抛出LimitExceededException。 |
11 |
达到管理限制 |
LimitExceededException |
12 |
不支持的关键扩展请求 |
OperationNotSupportedException |
13 |
需要机密信息 |
AuthenticationNotSupportedException |
14 |
SASL绑定中 |
由LDAP提供者在认证过程中使用。 |
16 |
属性不存在 |
NoSuchAttributeException |
17 |
未定义属性类型 |
InvalidAttributeIdentifierException |
18 |
不合适的匹配 |
InvalidSearchFilterException |
19 |
常量违例 |
InvalidAttributeValueException |
20 |
属性值正在使用中 |
AttributeInUseException |
21 |
属性语法错误 |
InvalidAttributeValueException |
32 |
对象不存在 |
NameNotFoundException |
33 |
别名错误 |
NamingException |
34 |
DN语法非法 |
InvalidNameException |
35 |
是叶子节点 |
LDAP提供者使用,通常不产生异常。 |
36 |
别名解析错误 |
NamingException |
48 |
不合适的认证 |
AuthenticationNotSupportedException |
49 |
机密信息非法 |
AuthenticationException |
50 |
访问权限不足 |
NoPermissionException |
51 |
忙 |
ServiceUnavailableException |
52 |
不可得 |
ServiceUnavailableException |
52 |
服务器不愿执行 |
OperationNotSupportedException |
54 |
检测到循环 |
NamingException |
64 |
命名违例 |
InvalidNameException |
65 |
对象类型违例 |
SchemaViolationException |
66 |
非叶子节点不允许操作 |
ContextNotEmptyException |
67 |
不允许在RDN上操作 |
SchemaViolationException |
68 |
条目存在 |
NameAlreadyBoundException |
69 |
对象类型禁止修改 |
SchemaViolationException |
71 |
影响多个DSA |
NamingException |
80 |
其他 |
NamingException |
LDAP服务提供一般的目录服务。可以用来保存任意种类的信息。所有的LDAP服务器有一些控制系统,可以从目录中读取和更新信息。
要访问LDAP访问,LDAP客户端必须通过服务器的认证。即,必须告诉LDAP服务器谁要访问数据,这样服务器就可以决定给客户端的权限。如果客户端成功的通过了LDAP服务器的认证,那么当服务器随后收到客户端的请求时,会检查客户端是否允许执行请求。这种过程叫做访问控制。
LDAP标准提出了LDAP服务器通过LDAP服务器认证的方式(RFC 2251和RFC 2829).它们在LDAP认证和认证方式一节说明。本节包括如何使用匿名用户,简单和SASL认证方式的描述。
访问控制被不同的LDAP服务器用不同的方式支持。本节不讨论这些内容。
LDAP服务安全的其他方面支持使用安全管道与客户端通信,例如发送和接收包含密钥属性,例如密码和key。为这个目的,LDAP服务器使用SSL。本节同时介绍如何为LDAP服务器提供者使用SSL。
在LDAP中,认证信息在“bind”操作中提供。在LDAP v2中,客户端通过向服务器发送包含认证信息的“bind”操作,初始化和LDAP服务器的联结。
在LDAPv3中,这个操作的目的一样,但不是必须的。如果客户端向服务器发送LDAP请求前没有“bind”,那么这个请求被当作匿名客户端处理(详细信息请参考匿名认证一节)。在LDAP v3中,“bind”操作可以在连接的任何时间发送,可能不止一次。客户端可以在连接的中间发送“bind”请求,用以改变身份。如果请求成功,那么连接中所有未处理的请求都被丢弃,将新的身份关联到连接中。
“bind”中提供的认证信息依赖于客户端选择的认证方式。详情请看下一节中关于认证技术的讨论。
在JNDI中,认证信息在环境属性中说明。当您使用InitialDirContext创建初始上下文时,您提供一组环境属性,其中一些可能包含认证信息。您可以使用以下环境属性描述认证信息。
l Context.SECURITY_AUTHENTICATION ("java.naming.security.authentication").
指定使用的认证方式。对于Sun 的LDAP服务提供者来说可以使用“none”,simple”, sasl_mech,其中sasl_mech是SASL方式名字的空格分割的字符串。这些字符串的描述请参考下一节。
l Context.SECURITY_PRINCIPAL ("java.naming.security.principal")
表示进行认证的用户/程序的名字,同时依赖于Context.SECURITY_AUTHENTICATION属性的值。详细信息请参考下一节。
l Context.SECURITY_CREDENTIALS ("java.naming.security.credentials")
表示进行认证用户/程序的机密信息,依赖于Context.SECURITY_AUTHENTICATION属性的值。详细信息请参考下一节。
当初始上下文创建后,LDAP服务提供者底层实现将认证信息从环境属性中取出,然后使用LDAP“bind”操作将它们发送到服务器。
以下例子说明客户端使用明文密码到LDAP服务器中进行认证。s
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Authenticate as S. User and password "mysecret" env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=S. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx |
如果您想为已经存在的上下文使用不同的认证信息,可以用Context.addToEnvironment()和Context.removeFromEnvironment()更新包含认证信息的环境属性。接下来上下文中请求的方法将使用新的认证信息和服务器通信。
以下例子展示了上下文建立后将上下文认证信息改为“none”的方式。
// Authenticate as S. User and the password "mysecret" env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=S. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
// Change to using no authentication ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "none");
// ... do something useful with ctx |
有很多原因可能导致认证失败。例如,如果您提供错误的认证信息(密码错误或认证名错误),那么会抛出AuthenticationException异常。
以下是上一个例子的变体。这次,错误的密码导致认证失败。
// Authenticate as S. User and give an incorrect password env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=S. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "notmysecret");
This produces the following output. javax.naming.AuthenticationException: [LDAP: error code 49 - Invalid Credentials] ... |
因为不同的服务器支持不同的认证方式,您可能请求了服务器不支持的认证方式。这种情况下,抛出AuthenticationNotSupportedException异常。
以下是上一个例子的变体。这次,不支持的认证方式(“custom”)导致认证失败。
// Authenticate as S. User and the password "mysecret" env.put(Context.SECURITY_AUTHENTICATION, "custom"); env.put(Context.SECURITY_PRINCIPAL, "cn=S. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret");
This produces the following output. javax.naming.AuthenticationNotSupportedException: custom ... |
不同的版本的LDAP支持不同的认证类型。LDAP v2支持三种认证类型:匿名,简单(明文密码)和Kerberos v4。
LDAP v3支持匿名,简单和SASL认证。SASL是简单认证和安全层(RFC 2222)。它定义了客户端可服务器之间为了认证的目的交换数据同时建立安全层的挑战-应答协议,安全层用在随后的通信中。使用SASL,LDAP可以支持LDAP客户端和服务器之间商量的任何类型的认证。
本节描述如何使用匿名,简单和SASL进行认证。
认证技术使用Context.SECURITY_AUTHENTICATION环境属性指定。属性可能是如下值中的一个。
l sasl_mech
空格分隔的SASL技术的名称列表。使用SASL技术列表中的一个(例如,“CRAM-MD5”的意思是使用RFC 2195中介绍的CRAM-MD5 SASL技术)。
l none
不进行认证(匿名)
l simple
使用弱的任何证方式(明文密码)
如果客户端没有指定任何认证环境属性,那么默认的认证方式是“none”。客户端被当作匿名客户端。
如果客户端指定认证信息没有特别说明Context.SECURITY_AUTHENTICATION属性,那么默认的认证方式是“simple”。
刚才已经说过,如果没有设置认证环境属性,那么默认的认证方式是“none”。入客户端设置Context.SECURITY_AUTHENTICATION环境属性为“none”,那么认证方式是“none”同时所有其他认证环境属性都被忽略。您这样做可以确保其他已经设置的认证属性被忽略。这两种情况下,客户端将被当作匿名客户端。这意味着服务器不知道或者不关心客户端是谁,并且允许客户端访问(读取和更新)任何设置为未授权用户可以访问的数据。
因为命名和目录操作一节中的例子没有设置任何环境属性,它们都被当作匿名认证。
这里是显式设置Context.SECURITY_AUTHENTICATION属性为“none”的例子(这样做不是很必要因为它是默认的)。
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Use anonymous authentication env.put(Context.SECURITY_AUTHENTICATION, "none");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx |
简单认证包含向LDAP服务器发送客户端(用户)的DN全名以及客户端明文密码(RFC 2251和RFC 2829)。这种方式会带来安全问题,因为密码可以从网络中读取。要避免这种方式暴露密码,您可以在加密管道中(例如SSL)使用简单认证方式,它是LDAP服务器提供的。
LDAP v2和v3都支持简单认证。
要使用简单认证方式,您必须设置如下三个认证环境参数。
l Context.SECURITY_AUTHENTICATION
设置为“simple”
l Context.SECURITY_PRINCIPAL
设置为用来认证的实体的DN全名(例如,“cn=S. User, ou=NewHires, o=JNDITutorial”)。它是一个java.lang.String。
l Context.SECURITY_CREDENTIALS
设置为主要的密码。类型为java.lang.String,char数组或byte数组。如果密码是java.lang.String或char数组,那么LDAPv3使用UTF-8或LDAP v2使用ISO-Latin-1向服务器发送数据。如果密码是byte[],那么就用这种方式传递到服务器。
请参考本节之前描述的有关于如何使用简单认证的例子。
注意:如果您Context.SECURITY_CREDENTIALS环境属性向提供空字符串,空byte/char数组或null,那么认证方式将是“none”。这是因为LDAP的简单认证密码不能为空。如果不提供,协议自动将认证方式改为“none”。
LDAP v3协议使用SASL支持插件认证。意味这LDAP客户端可服务器可以配置成协商使用不标准或自定义的认证方式,依赖于客户端和服务器需要的保护级别。LDAPv2协议不支持SASL。
现在定义了一些SASL认证方式:
l 匿名(RFC 2245)
l CRAM-MD5(RFC 2195)
l Digest-MD5(RFC 2831)
l External (RFC 2222)
l Kerberos V4 (RFC 2222)
l Kerberos V5 (RFC 2222)
l SecurID (RFC 2808)
l S/Key (RFC 2222)
和刚才列出的技术一样,流行的LDAP服务器(例如Sun,OpenLDAP以及微软)支持外部的,Digest-MD5和Kerberos V5。RFC 2829建议将Digest-MD5作为LDAPv3服务器强制的默认技术。
以下简单程序为了发现LDAP服务器支持的SASL技术列表。
// Create initial context DirContext ctx = new InitialDirContext();
// Read supportedSASLMechanisms from root DSE Attributes attrs = ctx.getAttributes( "ldap://localhost:389", new String[]{"supportedSASLMechanisms"}); |
以下是运行程序查看服务器支持的外部SASL技术的结果。
{supportedsaslmechanisms=supportedSASLMechanisms: EXTERNAL, GSSAPI, DIGEST-MD5} |
要使用某种SASL技术,您需要在Context.SECURITY_AUTHENTICATION环境属性中指定网络编号授权(IANA)注册的技术名。您可以指定LDAP提供者尝试的技术列表。这通过指定空格分隔的技术名称列表实现。LDAP提供者将使用列表中第一个自己实现的技术。
以下例子告诉LDAP提供者尝试得到DIGEST-MD5的实现,如果没有提供,使用GSSAPI。
env.put(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5 GSSAPI"); |
您可以从程序的用户那里得到认证技术。或者您可以询问LDAP服务器得到它,通过刚才展示的类似方式。LDAP提供者自己不考虑服务器,它只是简单尝试定位并且使用指定的技术实现。
平台中的LDAP提供者对于External, Digest-MD5和GSSAPI(Kerberos v5)SASL mechanisms提供内置的支持。您可以提供额外的技术。
一些技术,例如External,不需要额外的输入—使用单独的技术名称用来进行认证已经足够了。External的例子表名如何使用External SASL技术。
大多数其他技术需要额外的输入。技术不同,输入可能不同。以下是各种技术需要的通用输入。
l Authentication id:执行认证的实体标识。
l Authorization id:如果认证通过,进行何种访问授权检测的实体标识。
l Authentication credentials:例如,密码或密钥。
如果程序(例如代理服务器)的认证代表其他实体时,认证和授权可能不同。authentication id使用Context.SECURITY_PRINCIPAL环境属性指定,它是java.lang.String类型。
authentication id的password/key使用Context.SECURITY_CREDENTIALS环境属性指定,它使用java.lang.String,char数组或byte数组。如果密码是byte数组,那么使用UTF-8编码方式转换成char数组。
如果已经设置了“java.naming.security.sasl.authorizationId”属性,那么它的值就被当作authorization ID,它的值必须是java.lang.String。默认情况下,authorization ID是空串,标识让服务器根据客户端的认证机密信息生成authorization ID。
Digest-MD5的例子表名如何使用Context.SECURITY_PRINCIPAL以及Context.SECURITY_CREDENTIALS属性进行Digest-MD5认证。
如果一种技术需要已经描述的其他方式输入,那么您 需要定义技术使用的回调对象,您可以查看JNDI教程中的回调例子。本节的下一部分讨论如何使用SASL Digest-MD5认证技术。SASL策略,GSS API(Kerberos v5)以及CRAM-MD5都在JNDI教程中描述。
Digest-MD5认证是LDAPv3服务器(RFC 2829)需要的认证技术。因为SASL是LDAP v3(RFC 2251)的一部分,只支持LDAP v2的服务器不支持Digest-MD5。
Digest-MD5技术在RFC 2831中描述。它基于HTTP摘要认证(RFC 2617)。在Digest-MD5中,LDAP服务器发送数据中包含多种认证选项,而且会附带一个向LDAP客户端发送的特殊令牌。客户端通过发送加密应答回应,其中指定它选择的认证选项。这种方式的应答表名客户端知道密码。LDAP服务器随后界面和校验客户端应答。
要使用Digest-MD5认证技术,您必须按照下述方式设置认证环境参数。
l Context.SECURITY_AUTHENTICATION:
设置为“DIGEST-MD5”
l Context.SECURITY_PRINCIPAL:
设置为登录名。它是由服务器指定的格式。一些服务器支持使用用户登录id的格式,例如Unix或Windows登录页面的定义。其他的接受辨别名。然而,其他的使用RFC 2829中定义的ID认证格式。在那个RFC中,名称或者是字符串“dn:”,跟随用来认证实体的完全DN,或“u:”,随后是用户的ID。一些服务器支持多种格式。例如:“cuser”,“dn: cn=C. User, ou=NewHires, o=JNDITutorial”,“u: cuser”。这个属性的数据类型必须是java.lang.String。
l Context.SECURITY_CREDENTIALS
设置为登录密码(例如:“mysecret”)。它的类型为java.lang.String,char数组或byte数组。如果密码是java.lang.String或char[],那么使用UTF-8编码后发送给服务器。如果密码是byte[],那么直接传递发呕服务器。
以下例子展示了客户端如何使用Digest-MD5到LDAP服务器执行认证。
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Authenticate as C. User and password "mysecret" env.put(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5"); env.put(Context.SECURITY_PRINCIPAL, "dn:cn=C. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx |
注意:Sun Java目录服务器,v5.2支持使用名为密码的Digest-MD5认证技术。您必须设置在创建用户前设置密码加密方式。如果您使用已经创建的用户,那么就删除再创建一个。要使用管理员控制台设置加密模式,选择配置标签以及数据节点。在Password窗口中,为“密码加密”选项选择“不加密(明文)”。服务器接受简单用户名(实体“uid”属性的值存在)同时用户名的格式是“dn:”。详细情况请看服务器的文档。
realm定义了选择用来认证的认证实体(Context.SECURITY_PRINCIPAL)。服务器可能有多个realm。例如,大学的服务器可能有两个realm,一个是学生用户另一个是教师用户。realm配置直接由目录管理员进行。一些目录有默认的单一realm。例如,Sun Java目录服务器,v5.2,使用机器主机名的全名作为默认realm。
在Digest-MD5认证中,您必须到指定realm中认证。您可以使用下列认证环境属性指定realm。如果您不指定realm,那么将使用服务器提供的任意一个realm。
l java.naming.security.sasl.realm
设置为登录的realm。这是由部署决定的同时是服务器提供的大小写敏感的字符串。它定义了登录名(Context.SECURITY_PRINCIPAL)应该选择额realm或domain。如果realm不能匹配服务器提供的一个realm,那么认证失败。
下列例子展示了如何设置环境属性用来使用Digest-MD5结合指定realm进行认证。要让这个例子在您的环境中运行,不许修改源码中的realm值为您的目录服务器配置的realm。
// Authenticate as C. User and password "mysecret" in realm "JNDITutorial" env.put(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5"); env.put(Context.SECURITY_PRINCIPAL, "dn:cn=C. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret"); env.put("java.naming.security.sasl.realm", "JNDITutorial"); |
如果您需要使用私有保护以及其他SASL数息观念,在JNDI教程中都有讨论。
除了SASL认证外,大多数LDAP服务器允许通过SSL访问访问。SSL对于LDAP v2有特殊的用途,因为v2不支持SASL认证。
支持SSL的服务器通常使用两种方式支持SSL。一种最基本的放肆横,服务器除了支持普通(未保护)的端口之外还支持SSL端口。另一种方式是服务器通过使用Start TLS Extension(RFC 2830)支持SSL。这个选项只有LDAP v3服务器支持,在本节后面进行介绍。
默认情况下,Sun的LDAP服务提供者使用简单套接字与LDAP服务器通信。要使用SSL套接字进行请求,设置Context.SECURITY_PROTOCOL属性为“ssl”。
在下面的例子中,LDAP服务器提供SSL端口636。要运行这个例子,必须在LDAP服务器的636端口启用SSL。这个步骤通常由目录管理员执行。
服务器需求:LDAP服务器必须提供X.509 SSL 服务器证书同时开启SSL。通常情况下,您必须首先从证书权威组织(CA)得到服务器的签名证书。然后,遵循您目录供应商关于如何启用SSL的说明。不同的供应商使用不同的工具做这件事。
对于Sun Java 目录服务器 V5.2,使用管理员控制台的管理工具生成服务器证书签名请求(CSR)。然后。将CSR提交到CA得到X.509 SSL服务器证书。使用管理员控制台,将证书加入服务器的证书列表中。同时,如果您的服务器中没有可信任的CA列表,就安装CA证书。
使用管理员控制台中的配置标签页启用SSL。选择左边面板中的服务器。选择右侧的加密方式页。单击“Enable SSL for this server”以及“Use this cipher family: RSA”选项,确认为您进行证书认证的服务器有证书列表。
客户端需求:您需要确认客户端信任您使用的LDAP服务器。您必须在JRE的可信证书数据库中安装服务器证书(或者CA的证书)。以下是例子:
# cd JAVA_HOME/lib/security # keytool -import -file server_cert.cer -keystore jssecacerts |
了解如何使用安全工具,请参考安全一节。了解如何使用JSSE,请参考JSSE相关文档。
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:636/o=JNDITutorial");
// Specify SSL env.put(Context.SECURITY_PROTOCOL, "ssl");
// Authenticate as S. User and password "mysecret" env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=S. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx |
注意:如果您连接到服务器中不支持SSL的端口,那么程序会挂起。类似的,如果您将使用一般的套机子连接SSL套接字,那么程序也会挂起。这是SSL协议的特性。
除了通过使用Context.SECURITY_PROTOCOL属性请求使用SSL,您可以通过使用LDAPS URL请求使用SSL。LDAPS URL和LDAP URL类似,但URL的模式是“ldaps”而不是“ldap”。它指定连接LDAP服务器时使用SSL。在以下例子中,LDAP服务器在636端口提供SSL。要运行这个程序,您必须启用LDAP服务器636端口的SSL。
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
// Specify LDAPS URL env.put(Context.PROVIDER_URL, "ldaps://localhost:636/o=JNDITutorial");
// Authenticate as S. User and password "mysecret" env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=S. User, ou=NewHires, o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx |
LDAPS URL接受LDAP URL可以接受的任何东西。LDAP和LDAPS的详细信息请查看JNDI教程。
SSL在LDAP的底层提供认证和其他安全服务。如果认证已经由SSL完成,LDAP层可以通过External SASL技术使用SSL的认证信息。
以下例子和上一个SSL例子一样,但不使用简单认证,而是使用External SASL认证。要使用External,您不需要提供任何登录名或密码信息,因为将会从SSL中得到这些信息。
服务器需求:这个例子需要LDAP服务器允许基于证书的客户端认证。同时,服务器必须信任(CA)收到的客户端证书,同时必须支持将客户端证书中的拥有者辨别名映射到登录名。请按照您的目录提供者的说明完成上述步骤。
客户端需求:这个例子需要客户端拥有X.509 SSL客户端证书。更多的是,客户端必须将其保存为keystore文件的第一个密钥条目。如果条目是密码保护的,必须和keystore有同样的密码。关于JSSE keystores的详细信息,请参考JSSE文档。
// Set up the environment for creating the initial context Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:636/o=JNDITutorial");
// Principal and credentials will be obtained from the connection env.put(Context.SECURITY_AUTHENTICATION, "EXTERNAL");
// Specify SSL env.put(Context.SECURITY_PROTOCOL, "ssl");
// Create the initial context DirContext ctx = new InitialDirContext(env);
... |
要运行这个程序,以便于客户端使用证书进行认证,您必须提供(作为系统属性)包含客户端证书的keystore的位置以及密码。以下是运行程序的例子。
java -Djavax.net.ssl.keyStore=MyKeystoreFile \ -Djavax.net.ssl.keyStorePassword=mysecret \ External |
如果您不提供keystore,程序使用匿名认证方式,因为SSL中没有客户端机密细细腻。
这个例子展示了完成基于证书的客户端认证的基本步骤。更高级的方式可以通过编写自定义套接字工厂实现,工厂可以使用更灵活的方式访问客户端证书,可能使用LDAP条目。下一节描述在您的JNDI程序中如何使用自定义套接字工厂。
当使用SSL时,LDAP提供者默认使用javax.net.ssl.SSLSocketFactory作为套接字工厂,用来创建与服务器连接的SSL套接字,默认使用JSSE配置。JSSE可以通过多种方式自定义,详细介绍请看JSSE文档。然而,当自定义信息不足时需要花费很多时间,同时您需要对LDAP服务提供者使用SSL套接字或一般套接字的进行更多控制。例如,您可能需要套接字透串出防火墙,或JSSE套接字为信任和key stores使用nondefault缓存/获取策略。要设置LDAP服务提供者使用的套接字工厂实现,就要为“java.naming.ldap.factory.socket”属性设置为套接字工厂类的全名。这个累必须实现javax.net.SocketFactory抽象类并且提供getDefault()方法的实现,它返回了套接字工厂的示例。详细信息请看JSSE文档。
以下自定义套接字工厂生成一般套接字。
public class CustomSocketFactory extends SocketFactory { public static SocketFactory getDefault() {
System.out.println("[acquiring the default socket factory]"); return new CustomSocketFactory(); } ... } |
注意,这个例子当创建新的LDAP连接时就创建CustomSocketFactory实例。这对于一些应用程序以及套接字工厂来说是合适的。如果您向重用同样的套接字工厂,getDefault()应该返回一个单例。
要在JNDI程序中使用自定义套接字工厂,按照下列例子中的方式设置“java.naming.ldap.factory.socket”属性。
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Specify the socket factory env.put("java.naming.ldap.factory.socket", "CustomSocketFactory");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx |
“java.naming.ldap.factory.socket”属性可以用来为上下文设置套接字工厂。LDAP服务提供者使用的其他控制套接字的方式是使用java.net.Socket.setSocketImplFactory()方法在整个socket程序中设置套接字工厂。这种方式不是十分灵活因为它影响了所有的socket连接,不只是LDAP连接,应该谨慎对待。
LDAP课程的其他部分包含如何使用JNDI进行某些LDAP操作。
您使用Context.rename()方法在目录中重命名对象。在LDAP v2中,对应于“modify RDN”操作是在同一个上下文中重命名条目(即,重命名兄弟)。在LDAP v3中,对应于 “modify RDN”操作,原有和新条目不在同一个上下文中。您可以使用Context.rename()重命名一个叶子条目或一个内部结点。这个例子展示了在命名和目录操作一颗中重命名一个叶子节点。以下代码将内部节点“ou=OldHires”重命名为“ou=NewHires”:
ctx.rename("ou=NewHires", "ou=OldHires"); |
注意:Sun Java目录服务器v5.2不支持重命名内部节点。如果运行这个异常,会抛出ContextNotEmptyException。
在LDAPv3中,您可以将条目重命名到DIT的不同地方。使用Context.rename()完成这个任务,您必须使用使用新旧条目的共同祖先完成。例如,要将“cn=C. User, ou=NewHires, o=JNDITutorial”重命名为“cn=C. User, ou=People, o=JNDITutorial”,您必须使用“o=JNDITutorial”上下文。以下例子证明了这个问题。如果您尝试在LDAP v2的服务器中运行这个例子,那么会得到InvalidNameException异常,因为v2不支持这个特性。
ctx.rename("cn=C. User, ou=NewHires", "cn=C. User, ou=People"); |
注意:Sun Java目录服务器v5.2不支持在不同父节点中进行重命名。如果您使用它来运行这个例子,会得到OperationNotSupportedException(表示“协议错误“)。
在LDAP中,但您重命名条目时,您可以选择是否将条目原有的RDN作为更新后条目的一个属性。例如,如果您将条目“cn=C. User”重命名为“cn=Claude User”,您可以指定是否将原有RDN“cn=C. User”保留为一个数息观念。
要指定当使用Context.rename()时是否将原有属性名保留为一个属性,使用“java.naming.ldap.deleteRDN”环境属性。如果属性值为“true”(默认),原有的RDN就被删除。如果值为“false”,那么原有RDN保留为更新后条目的一个属性。完整的例子如下:
// Set the property to keep RDN env.put("java.naming.ldap.deleteRDN", "false");
// Create the initial context DirContext ctx = new InitialDirContext(env);
// Perform the rename ctx.rename("cn=C. User, ou=NewHires", "cn=Claude User,ou=NewHires"); |
LDAP“compare”操作允许客户端询问服务器是否有一个条目有指定的名/值对。这样允许服务器保持一些属性/值对是秘密的(例如,不为一般的“search”访问暴露),当仍饭允许客户端首先的使用它们。一些服务器对于密码使用这种特性,例如,即使在“compare”操作中将明文密码从客户端发送到服务器是不安全的。
要在JNDI中完成这项工作,使用以下方法的合适组合就可以了:
search(Name name, String filter, SearchControls ctls) search(Name name, String filterExpr, Object[]filterArgs, SearchControls ctls) |
过滤器必须为“(name=value)”的形式。不能使用通配符。
搜索范围必须为SearchControls.OBJECT_SCOPE。
您的请求必须没有任何返回。如果这些条件不满足,那么将执行LDAP“search”操作而不是LDAP“compare”操作。
以下是LDAP“compare”操作的例子。
// Value of the attribute byte[] key = {(byte)0x61, (byte)0x62, (byte)0x63, (byte)0x64, (byte)0x65, (byte)0x66, (byte)0x67};
// Set up the search controls SearchControls ctls = new SearchControls(); ctls.setReturningAttributes(new String[0]); // Return no attrs ctls.setSearchScope(SearchControls.OBJECT_SCOPE); // Search object only
// Invoke search method that will use the LDAP "compare" operation NamingEnumeration answer = ctx.search("cn=S. User, ou=NewHires", "(mySpecialKey={0})", new Object[]{key}, ctls); |
如果比较成功,结果将会返回一个item,名字为空,不包含属性。
当您使用DirContext中的search方法,将得到NamingEnumeration。NamingEnumeration的每一个SearchResult中包含以下信息:
l Name
l Object
l Class name
l Attributes
每个SearchResult包含满足搜索过滤器的LDAP条目名。使用getName()方法得到条目的名称。这个方法返回相对于目标上下文LDAP条目的组合名称。在LDAP中,目标上下文是搜索的基本对象。例如:
NamingEnumeration answer = ctx.search("ou=NewHires", "(&(mySpecialKey={0}) (cn=*{1}))", // Filter expression new Object[]{key, name}, // Filter arguments null); // Default search controls |
本例中,目标上下文的名字是“ou=NewHires”。SearchResults中的名字都相对于“ou=NewHires”。例如,如果getName()返回“cn=J. Duke”,那么它的名字相对于ctx将会是“cn=J. Duke, ou=NewHires”。
如果您使用SearchControls.SUBTREE_SCOPE或SearchControls.OBJECT_SCOPE执行搜索,同时目标上下文满足搜索过滤器,那么返回的名称将会是“”(空字符串),因为它相对于目标上下文。
这不是全部。如果搜索涉及referrals或解析引用(参考JNDI教程),那么对应的SearchResults将有不与目标上下文关联的名字。取而代之的是条目直接引用的URL。要确定getName()返回的名字是相对的还是绝对的,使用isRelative()。如果这个方法返回true,表示名字相对于目标上下文;如果返回false,表示名字是一个URL。
如果名字是URL并且您需要使用URL,那么您可以将它传入可以解析URL的初始上下文中(请参考JNDI教程)。
如果您需要得到条目的完整DN,使用NameClassPair.getNameInNamespace()。
如果搜索引导返回条目的对象(SearchControls.setReturningObjFlag()为true),那么SearchResult将会包含表示条目的对象。要取出对象,您可以请求getObject()。如果java.io.Serializable, Referenceable或引用对象先前绑定到LDAP名称中,那么条目的属性用来重建对象(请参考JNDI教程中的例子)。否则,条目中的属性就用来创建表示LDAP条目的DirContext对象。在这两种情况下,LDAP提供者请求对象的DirectoryManager.getObjectInstance()方法并且返回结果。
如果搜索由请求返回的条目对象引导,那么类名源于返回对象。如果搜索请求属性中包含获取LDAP条目的“javaClassName”属性,那么类名是这个属性的值。否则,类名是“javax.naming.directory.DirContext”。使用getClassName()获得列名。
当执行搜索时,您可以将需要返回的属性放在search()的方法的一个参数中或使用SearchControls.setReturningAttributes()设置search control的参数。如果没有特别指定需要返回的参数,整个LDAP条目都会返回。要指定不返回任何数息观念,您必须传递空的数组(new String[0])。
要获取LDAP条目属性,您可以请求SearchResult的getAttributes()方法。
请参考JNDI教程的控制和扩展一节中关于如何从搜索中得到应答control的详细信息。
LDAPv3(RFC 2251)定义了unsolicited notification,它是由LDAP服务器发送给客户端的报文,这个报文不需要客户端的任何激发。JNDI中使用UnsolicitedNotification接口表示未请求的通知。
因为未请求的通知由服务器异步的发送,您可以使用同样的时间模型接受关于命名空间改变和对象内容改变的通知。您可以使用EventContext或EventDirContext将感兴趣的未请求通知注册到UnsolicitedNotificationListener中。
以下是UnsolicitedNotificationListener的例子:
public class UnsolListener implements UnsolicitedNotificationListener { public void notificationReceived(UnsolicitedNotificationEvent evt) { System.out.println("received: " + evt); }
public void namingExceptionThrown(NamingExceptionEvent evt) { System.out.println(">>> UnsolListener got an exception"); evt.getException().printStackTrace(); } } |
以下是注册UnsolicitedNotificationListener实现的例子。注意,只有监听器的参数到EventContext.addNamingListener()是有关的。名称和范围参数和未请求通知无关。
// Get the event context for registering the listener EventContext ctx = (EventContext) (new InitialContext(env).lookup("ou=People"));
// Create the listener NamingListener listener = new UnsolListener();
// Register the listener with the context (all targets equivalent) ctx.addNamingListener("", EventContext.ONELEVEL_SCOPE, listener); |
当运行程序时,您需要指向一个可以生成未请求通知的LDAP服务器,同时让服务器发送通知。否则,一分钟后程序静默离开。
UnsolicitedNotificationListener的实现也可以实现其他NamingListener接口,例如NamespaceChangeListener和ObjectChangeListener。
JNDI提供了访问命名和目录服务的高级接口。
在JNDI上下文实例和底层网络连接之间的映射可能不是一对一的。服务提供者可以自由的分享以及重用连接,只要维持接口的语意就可以。应用程序开发者不需要了解context如何创建连接的细节。这些细节在开发者进行调优时很有用。
本节描述了LDAP服务提供者如何使用连接。包括连接如何创建,如何指定连接参数,比如多服务器或连接超时。本节同时说明在网络环境中如何动态发现和使用支持的LDAP服务器。
建立连接的数量和关闭的必须一致。本节包含客户端和服务器关闭连接的描述。
最后,本课描述如何使用连接池让应用程序更有效的使用多个短连接。
注意:本课中的信息是对于Sun的LDAP服务提供者来说的。其他供应商的LDAP服务提供者可能不使用同样的策略进行连接管理。
有多种方法创建连接。最常用的方式是创建初始上下文。当您使用LDAP服务提供者创建InitialContext,InitialDirContext或InitialLdapContext时,立即使用Context.PROVIDER_URL属性提供的URL建立起一个和LDAP服务器的连接。每次初始化上下文时,都创建一个新的LDAP连接。请参考连接池一节得到如何改变这种特性的信息。
如果属性值中包含多于一个URL,那么使用每个URL进行轮流测试,直到连接成功建立。然后将属性值更新为可以正确连接的URL。请看JNDI教程中关于如何使用一组URL创建初始上下文的例子。
创建连接有其他三种直接的方式。
1. 向初始上下文使用相同参数传递URL。当LDAP或LDAP URL作为一个命名参数传递到初始上下文时,将使用URL中的信息创建到LDAP服务器的新连接,不管初始上下文实例自己是否已经有了到LDAP服务器的连接。实际上,初始上下文可能不连接任何服务器。请参见JNDI教程中关于如何象名字一样使用URL。
2. 另一个方式是使用引用创建连接。当包含LDAP或LDAPS URL的引用传递到NamingManager.getObjectInstance()实例或DirectoryManager.getObjectInstance()实例后,使用URL中的信息创建新连接。
3. 最后,但自动或手动跟随referral时,使用referral中的信息创建新连接。请参考JNDI教程中referral中的信息。
Context实例和源于同一个Context实例的NamingEnumerations共享相同的连接,直到其中一个Context实例让连接变得不可能共享之前。例如,如果您从初始上下文请求Context.lookup(),Context.listBindings()或DirContext.search()并且得到其他Context实例,那么所有的Context实例共享同一个连接。
例如:
// Create initial context DirContext ctx = new InitialDirContext(env);
// Get a copy of the same context Context ctx2 = (Context)ctx.lookup("");
// Get a child context Context ctx3 = (Context) ctx.lookup("ou=NewHires"); |
在本例中,ctx,ctx2和ctx3共享同一个连接。
不管Context实例如何出现的都可以共享。例如,一个Context实例通过跟随referral得到,那么它将会和referral共享相同的连接。
当您修改Context实例的中和连接相关的环境属性时,例如用户的登录名或机密信息,那么进行这些修改的Context实例将会得到自己的连接(如果连接是共享的)。将来源于这个Context的Context实例会共享新的连接。原来共享旧连接的Context实例不会收到影响(即,继续使用原来的连接)。
以下是使用两个连接的例子:
// Create initial context (first connection) DirContext ctx = new InitialDirContext(env);
// Get a copy of the same context DirContext ctx2 = (DirContext)ctx.lookup("");
// Change authentication properties in ctx2 ctx2.addToEnvironment(Context.SECURITY_PRINCIPAL, "cn=C. User, ou=NewHires, o=JNDITutorial"); ctx2.addToEnvironment(Context.SECURITY_CREDENTIALS, "mysecret");
// Method on ctx2 will use new connection System.out.println(ctx2.getAttributes("ou=NewHires")); |
ctx2最初和ctx共享同样的连接。但当它的登录名和密码属性修改后,不能再使用ctx的连接。LDAP提供者自动为ctx2创建新连接。
类似的,如果您使用LdapContext.reconnect()修改Context实例的连接控制,如果连接被共享,Context实例将会得到自己的连接。
如果Context实例的连接没有共享(例如,没有源于这个Context的Context),那么对于环境属性的或连接控制的修改不会建立新的连接。取而代之的是,已经存在的连接将会应用新的属性。
不是所有连接都正常建立。如果LDAP提供者不能在超时时间内建立连接,会放弃连接尝试。默认情况下,超时时间是网络(TCP)超时时间,它大概是几分钟。要改变超时时间,您可以使用“com.sun.jndi.ldap.connect.timeout”环境属性。这个属性的值是整型字符串,表示连接超时的毫秒数。
例如:
// Set up environment for creating initial context Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Specify timeout to be 5 seconds env.put("com.sun.jndi.ldap.connect.timeout", "5000");
// Create initial context DirContext ctx = new InitialDirContext(env);
// do something useful with ctx |
在本例中,如果连接不能在5秒内建立,会抛出异常。
如果Context.PROVIDER_URL属性包含多于一个URL,提供者将为每个URL使用这个超时时间。例如,如果有3个URL同时超时时间为5秒,那么提供者最多需要等待15秒。
关于这个属性如何影响连接池,请参考连接池一节。
当Context实例不再使用时,一般的垃圾收集器小心的移除它们。Context实例使用的连接在垃圾收集时会自动关闭。因此,您不需要显式的关闭连接。然而,网络连接是有限的资源,在某些程序中,您可能向控制它们的生成和使用。本节包含关于如何关闭连接和当关闭连接时如何得到通知的信息。
您在Context实例中请求Context.close()方法,表示您不再使用它。如果Context实例已经使用专注的连接关闭了,也可以关闭。如果Context实例和其他Context以及未终止的NamingEnumeration实例共享,这个连接不会被关闭,直到所有这种的Context和NamingEnumeration中都请求close()方法后才会关闭。
在创建连接一节的例子中,所有的连接实例必须在底层连接关闭前关闭。
// Create initial context DirContext ctx = new InitialDirContext(env);
// Get a copy of the same context Context ctx2 = (Context)ctx.lookup("");
// Get a child context Context ctx3 = (Context) ctx.lookup("ou=NewHires");
// do something useful with ctx, ctx2, ctx3
// Close the contexts when we're done ctx.close(); ctx2.close(); ctx3.close(); |
在前文中提到,对于不在范围内的Context和NamingEnumeration实例,Java运行时会最终进行垃圾收集,因此close()将会清除状态。要强制进行垃圾收集,您可以使用下列代码:
Runtime.getRuntime().gc(); Runtime.getRuntime().runFinalization(); |
依赖于程序的状态,执行这个过程可能导致严重的(临时的)性能下降。如果您需要保证连接关闭,跟踪Context实例,并且进行显式关闭。
LDAP在关闭不使用的连接前会有一段空闲时间。当您随后在Context实例中使用这个连接请求方法时,方法会抛出CommunicationException。要检测服务器关闭的Context正在使用的连接,您可以为Context对象注册UnsolicitedNotificationListener。LDAP未请求通知一节中已经有例子。虽然例子的目的是接受服务器的未请求通知,它同时可以用来检测服务器关闭连接。在开始程序后,关闭LDAP服务器同时可以检测到监听器的namingExceptionThrown()方法被调用。
创建连接一节描述了什么时候连接创建。它描述和多个Context如何共享连接。
另一种LDAP服务提供者支持的连接共享类型叫做连接池。在这种共享方式中,LDAP服务提供者维护一个先前使用的连接池,并且将他们赋值给需要的Context实例。当Context实例已经处理完毕(关闭或垃圾收集),连接回到池中未将来使用。注意,这种共享方式是按顺序进行的:从池中获取一个连接,使用,归还,然后,被另一个Context实例获取。
连接池在每一个Java运行时中维护。在一些解决方案中,连接池可以极大的提高系统性能。例如,当搜索应答中包含四个指向同一个LDAP服务器的referral时,如果使用连接池,只需要一个连接。如果没有连接池,这种场景下需要四个不同的连接。
剩下的课程中将会详细描述如何使用连接池。
你可以通过向初始上下文的构造函数传递参数的方式使用连接池,参数参数为“com.sun.jndi.ldap.connect.pool”。示例如下:
// Set up environment for creating initial context Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Enable connection pooling env.put("com.sun.jndi.ldap.connect.pool", "true");
// Create one initial context (Get connection from pool) DirContext ctx = new InitialDirContext(env);
// do something useful with ctx
// Close the context when we're done ctx.close(); // Return connection to pool
// Create another initial context (Get connection from pool) DirContext ctx2 = new InitialDirContext(env);
// do something useful with ctx2
// Close the context when we're done ctx2.close(); // Return connection to pool |
这个例子连续创建了两个初始连接。第二个初始上下文使用第一个连接。要运行这个程序并且得到连接怎样得到和归还到池中的调试信息,使用以下的命令行。
#java -Dcom.sun.jndi.ldap.connect.pool.debug=fine UsePool |
顺序可能如下所示:
Create com.sun.jndi.ldap.LdapClient@5d173[localhost:389] Use com.sun.jndi.ldap.LdapClient@5d173 {ou=ou: NewHires, objectclass=objectClass: top, organizationalUnit} Release com.sun.jndi.ldap.LdapClient@5d173 Use com.sun.jndi.ldap.LdapClient@5d173 {ou=ou: People, objectclass=objectClass: top, organizationalunit} Release com.sun.jndi.ldap.LdapClient@5d173 |
您可以通过包括或忽略“com.sun.jndi.ldap.connect.pool”属性决定什么时候什么地方使用池技术,这样可以根据条件使用池技术。在上一个例子中,如果您在第二个初始上下文创建前从环境属性中移除这个参数,第二个初始上下文将不会使用池中的连接。
LDAP提供者在程序中一直跟踪连接的使用。好像是应用程序维护了一个开放的用来连接的上下文句柄。因此,为了让LDAP提供者合适的管理池中的连接,您必须当context不用时请求Context.close()方法。
LDAP提供者自动检测并且移除没用的连接。不管是否使用连接池,上下文因为坏的连接结束的概率是一致的。
LDAP服务提供者维护的连接池可能有大小限制,这在连接池配置一节进行详细描述。当启用连接池但是池中没有可用连接时,客户端程序会阻塞,等待得到新连接。您可以使用“com.sun.jndi.ldap.connect.timeout”环境属性配置等待池中连接的时间。如果您省略这个属性,应用程序将无限等待。
相同的属性也用来指定建立LDAP连接的超时时间,这在创建连接一节描述。
连接池的目的是重用。但是,如果在Context实例中计划执行的操作可能修改底层网络状态,那么那么Context实例不应该使用连接池。例如,如果您在上下文实例中计划请求Start TLS extended操作,或在初始上下文创建后想要修改安全相关属性(例如,“java.naming.security.principal”或“java.naming.security.protocol”),您不应该为这些Context实例使用连接池,因为LDAP提供者不能跟踪任何状态改变。
如果您在这种情况下使用连接池,可能会为引用程序带来安全问题。
连接池在Java运行时进行配置和维护。连接不会跨运行时共享。要使用连接池,不需要任何配置。如果您想要自定义池的行为,例如控制池的大小以及池中连接的类型,这时需要进行一些配置。
使用程序启动时的一些系统属性进行池的配置。注意,这些都是系统属性,不是环境属性,他们影响所有的连接池请求。
下面的例子在命令行中设置池的最大大小是20,合适的大小是10,池中连接的空闲时间是1分钟。
# java -Dcom.sun.jndi.ldap.connect.pool.maxsize=20 \ -Dcom.sun.jndi.ldap.connect.pool.prefsize=10 \ -Dcom.sun.jndi.ldap.connect.pool.timeout=60000 \ UsePool |
下表展示了用来配置连接池的系统属性。他们在剩下章节中进行详细介绍。
系统属性名 |
描述 |
默认值 |
com.sun.jndi.ldap.connect.pool.authentication |
池中对象的连接认证类型,是空格分隔的列表。合法值为“none”,“simple”和“DIGEST-MD5” |
“none simple” |
com.sun.jndi.ldap.connect.pool.debug |
一个表示调试信息输出级别的字符串。合法值为“fine”(跟踪连接创建和关闭)以及“all”(所有的调试信息) |
|
com.sun.jndi.ldap.connect.pool.initsize |
一个字符串类型的整数,表示初始化时创建连接的数量 |
1 |
com.sun.jndi.ldap.connect.pool.maxsize |
一个字符串类型的整数,表示同时维护连接的最大数量 |
没有限制 |
com.sun.jndi.ldap.connect.pool.prefsize |
一个字符串类型的整数,表示应该同时维护的连接数量 |
没有限制 |
com.sun.jndi.ldap.connect.pool.protocol |
空格分隔的连接池中的连接协议类型。合法值为“plain”和“ssl” |
“plain” |
com.sun.jndi.ldap.connect.pool.timeout |
一个字符串类型的整数,表示池中连接在关闭或移除前空闲的最大时间 |
没有超时 |
当您的Context实例使用“com.sun.jndi.ldap.connect.pool”环境属性配置的连接池时,连接可能被池化或不被。默认的规则是一般的(non-SSL)连接,使用简单的或未授权的连接允许被池化。您可以修改系统属性默认值,让它包含SSL和DIGEST-MD5认证类型。为了同时允许一般的和SSL连接都被池化,设置“com.sun.jndi.ldap.connect.pool.protocol”系统属性为“plain ssl”。要让匿名(none),简单和DIGEST-MD5认证类型的连接被池化,设置“com.sun.jndi.ldap.connect.pool.authentication”系统属性为“none simple DIGEST-MD5”。
有很多环境属性自动的让Context实例失去使用连接池的资格。如果设置“java.naming.ldap.factory.socket”属性而使用自定义的套接字工厂类,或设置“java.naming.security.sasl.callback”属性而使用自定义回调句柄类,或设置“com.sun.jndi.ldap.trace.ber”属性启用协议跟踪,那么Context实例不能使用连接池。
当Context实例请求使用池中连接时,LDAP提供者需要确定请求使用满足使用池中连接的条件。它通过将连接标识符赋予每一个池中连接,同时检查请求是否和池中连接有相同的连接标识符。
连接标识符是创建认证的LDAP连接需要的一组参数。它的组合依赖于请求的认证类型,详细信息请看下表:
认证类型 |
连接标识符内容 |
none |
l 连接控制 l 向初始上下文提供的“java.naming.provider.url”属性中的主机名和端口号,referral或URL。 l 以下属性的内容: n java.naming.security.protocol n java.naming.ldap.version |
simple |
l none中所有的信息列表 l 以下属性的内容: n java.naming.security.principal n java.naming.security.credentials |
DIGEST-MD5 |
l simple中列出的所有信息 l 以下属性的内容: n java.naming.security.sasl.authorizationId n java.naming.security.sasl.realm n javax.security.sasl.qop n javax.security.sasl.strength n javax.security.sasl.server.authentication n javax.security.sasl.maxbuffer n javax.security.sasl.policy.noplaintext n javax.security.sasl.policy.noactive |
LDAP提供者维护连接池;每个池中保存有同样连接标识的连接,不管是空闲的还是使用中的。有三个大小影响对池的控制。这些大小是全局的并且影响所有的池。
初始的池大小是LDAP提供者在第一次创建池时(对应于第一次请求这种连接标识的池中连接时创建的)每种连接标识创建的连接数量。池中每一个连接在连接使用时按照要求进行认证。默认情况下,初始池大小是1并且可以使用环境属性“com.sun.jndi.ldap.connect.pool.initsize”进行修改。通常在程序开始时事先让池有到服务器的连接。
最大池大小是LDAP访问提供者同时维护的每种连接标识的最大连接数。正在使用的和空闲连接组成这个数量。当池的大小达到这个数量时,相应池标识的连接不会在创建新连接,直到从池中移除一个连接(例如,物理连接被关闭)。当池大小达到最大并且池中所有连接都在使用时,从池中请求连接的应用程序被阻塞,直到池中的一个连接变成空闲或被移除。最大池大小为0意味着没有最大大小:池中的一个连接请求将使用存在的空闲连接或新创建一个连接。
合适的池大小是LDAP服务提供者应该维护的每种连接标识的连接数量。正在使用的和空闲连接组成这个数值。当程序请求使用一个池中的连接并且池的大小小于合适大小时,LDAP提供者将会创建新连接,而不管是否有空闲连接可用。当使用池中连接的应用程序结束(共享连接的所有上下文请求Context.close()方法),同时池的大小大于合适大小,LDAP提供者将会关闭并且从池中移除连接。合适大小为0表示没有合适大小:如果没有空闲连接,对池中连接的请求将会创建新的连接。
注意最大池大小覆盖了初始大小和合适的池大小。例如,设置合适池大小比最大池大小大等于将他设置为最大大小。
当使用池中连接的应用程序结束时(共享连接的所有上下文请求Context.close()方法),底层池连接变为空闲,等待被重用。默认情况下,空闲连接一直保留在池中,直到被垃圾收集。如果设置“com.sun.jndi.ldap.connect.pool.timeout”系统属性,LDAP提供者会自动关闭并且移除池中超过特定时间的空闲连接。
本节回到使用JNDI访问LDAP服务时经常问到的问题。一些常见问题在命名和目录操作一节问题解答部分已经做过回到。
1. 上下文是否可以安全的在多线程环境下访问,或者是否应该在访问时进行锁定或同步?
答案依赖于事先。因为Context和DirContext接口不指定同步需要。Sun的LDAP实现为单线程访问做过优化。如果您使用多个线程访问同一个Context实例,那么每一个线程在使用Context实例时应该加锁。这同样可以应用到源于同一个Context实例的NamingEnumeration对象中。然而,多线程可以访问不同的Context实例(即使他们源于同一个初始上下文)不需要同步锁。
2. 为什么当我不配置Context.SECURITY_CREDENTIALS(“java.naming.security.credentials”)或配置空字符串时LDAP供应商忽略所有的环境属性?
如果您向Context.SECURITY_CREDENTIALS环境属性提供空字符窜、空byte/char数组、或null,那么就算Context.SECURITY_AUTHENTICATION属性设置为“simple”,也会执行匿名绑定。这时因为对于simple认证,LDAP需要密码非空。如果没有提供密码,那么协议自动将认证方式转换成“none”。
3. 为什么当我尝试创建初始上下文时出现CommunicationException?
您可能和只支持LDAP v2的服务器对话。请参考JNDI中关于如何设置版本号的例子。
4. 如何跟踪LDAP消息?
试试使用“com.sun.jndi.ldap.trace.ber”环境属性。如果这个属性的值是java.io.OutputStream对象,那么LDAP提供者用户发送和接收的BER缓冲区写入这个流中。如果这个属性为空,那么不写任何输出信息。
例如,如下代码将跟踪输入发送到System.err:
env.put("com.sun.jndi.ldap.trace.ber", System.err); |
5. 怎样使用不同的认证方式,例如Kerberos?
按照JNDI教程的GSS-API/Kerberos v5认证中关于如何使用Kerberos认证的说明。要使用其他的认证方式,请参考JNDI教程中使用任意的SASL方式的章节。
6. 当我修改密码时是否应该启用SSL?
这依赖于您使用的目录服务器。一些目录服务器不允许在没有启用SSL的条件下修改密码,但有一些允许。启用SSL让密码在通信信道的保护下传送是一个很好的选择。
7. 当我请求一个属性时却得到另一个,为什么?
您使用的属性名可能是其他属性的同义词。这种情况下,LDAP服务器可能返回官方的属性名而不是您提供的。当您关注于服务器返回的属性时,您需要使用官方的名称而不是同义词。
例如,“fax”可能是官方名为“facsimiletelephonenumber”的属性的同义词。如果您使用“fax”进行请求,服务器将会返回名为“facsimiletelephonenumber”的属性。请参考命名和目录操作一些得到同义词以及关于属性名的其他描述。
8. 我怎样直到属性值的类型?
属性值的可以是java.lang.String或byte[]。请参考JNDI教程其他章节中关于什么属性返回byte[]的描述。要在程序中判断,可以使用instanceof操作符去检查您从LDAP提供者得到的属性值。
9. 怎样得到某种格式的属性值,而不是String或byte数组?
现在不行。LDAP提供者只返回java.lang.String或byte[]类型。请参考JNDI教程中其他一章。
10.为什么在属性值中赋值“*”在搜索中不生效?
当您使用以下格式的search(),属性值被当作字面值;即,目录条目中的属性值期待包含精确值:
search(Name name, Attributes matchingAttrs) |
要使用通配符,您应该使用字符串过滤器格式的search(),如下:
search(Name name, String filter, SearchControls ctls) search(Name name, String filterExpr, Object[]filterArgs, SearchControls ctls) |
在最后一种形式中,通配符必须出现在filterExpr参数中,不再filterArgs中。filterArgs中的值当作字面值处理。
11.为什么搜索过滤器中的通配符不总工作?
在属性值前后(例如“attr=*I*”)出现的属性表示服务器使用属性的字串匹配规则搜索匹配的属性值。如果属性的定义不包含字串匹配规则,那么服务器不能找到属性。您可以在搜索中使用等号或“存在”的过滤器代替。
12.为什么我只能得到n个值,但我直到目录中有更多值?
一些服务器配置了可以返回条目的数量限制。有一些也限制了在搜索中可以检查的条目数量。请检查您服务器的配置。
13.如何将control传递到搜索中?
本手册中不解释Control。请在JNDI教程中登记。
14.我怎么直到得到了多少搜索结果?
您必须在枚举结果时进行计数。LDAP不提供这些信息。
15.为什么在我的SearchResult中得到空字符串?
getName()总是返回相对于搜索的目标上下文的名称。所以,如果目标上下文满足搜索过滤器,那么返回的名称将是“”(空的名称),因为这个名称是相对于目标上下文的。详细信息请参考搜索结果一节。
16.为什么在我的SearchResult的名称中得到一个URL字符串?
LDAP条目需要跟随别名或referral取得,所以名称是URL。详细信息请参考搜索结果一节。
17.传递给Context和DirContext方法的命名参数类型是什么?- CompoundName还是CompositeName?
字符串格式接受混合名字表示的字符串。即,使用字符串名字等于请求new CompositeName(stringName)同时将结果传递到Context/DirContext方法中。Name参数可以是任何实现Name接口的对象。如果是CompositeName对象,那么名字作为composite name对待否则当作compound name对待。
18.能否将从NameParser得到的名字传递到Context方法中?
这和上一个问题关联。是,您可以。NameParser.parse()返回实现Name接口的compound name。这个名字可以传递到Context的方法中,作为compound name解析。
19.Context.SECURITY_PRINCIPAL属性和目录中使用的名字有什么关系?
您可以想象成不同命名空间的登录名,而不是目录。请参考RFC 2829或LDAP认证计数的安全部分得到详细结果。Sun的LDAP服务提供者接受字符串的登录名,它是直接传递到LDAP服务器的。一些LDAP服务器接受DN,而其他的支持RFC 2829中提出的模式。
20.为什么我从目录中读取的名字有奇怪的引号标记?
Sun的LDAP解析器关于引号规则进行保留处理,尽管如此也产生“正确的”名字。同时,记住NamingEnumerations返回的条目名字是composite names可以在传递到Context或DirContext的方法中。所以,如果名字包含和composite name语法冲突的字符(例如斜杠“/”),那么LDAP提供者会提供一种编码保证斜杠被当作LDAP名称的一部分,而不是composite name分隔符。
开始使用LdapName或Rdn类可以容易的进行名字操作。
21.如何得到LDAP条目的完整DN?
使用NameClassPair.getNameInNamespace()。