技术开发 频道

目录服务与JNDI


IT168技术文档】 
    随着信息技术在企业中日益广泛的应用,企业里的IT资源也变得纷繁复杂起来。企业的资源包括打印机,扫描仪,计算机,路由器,交换机,部门,员工等都有的种类。分散管理这些资源正变得越来越困难。
我们需要一个统一的机制来控制和管理以及保卫资源的信息。这时候就有了目录服务。比较常见的网络操作系统都注意到了这一问题,因此也引入了相应的解决方案。可惜好多企业并没有意识到目录服务的重要性,一般来说都是一堆的 98 和 2000 机器,乱七八糟,没有统一的管理。 

    LDAP简介: 
    LDAP的英文全称是Lightweight Directory Access Protocol,一般都简称为LDAP。它是基于X.500标准的,
但是简单多了并且可以根据需要定制。与X.500不同,LDAP支持TCP/IP,这对访问Internet是必须的。LDAP 的核心规范在RFC中都有定义,所有与LDAP相关的RFC都可以在LDAPman RFC网页中找到。现在LDAP技术不仅发展得很快而且也是激动人心的。在企业范围内实现LDAP可以让运行在几乎所有计算机平台上的所有的应用程序从LDAP目录中获取信息。LDAP目录中可以存储各种类型的数据:电子邮件地址、邮件路由信息、人力资源数据、公用密匙、联系人列表,等等。通过把LDAP目录作为系统集成中的一个重要环节,可以简化员工在企业内部查询信息的步骤,甚至连主要的数据源都可以放在任何地方。 

    LDAP目录的优势 

    如果需要开发一种提供公共信息查询的系统一般的设计方法可能是采用基于WEB的数据库设计方式,即前端
使用浏览器而后端使用WEB服务器加上关系数据库。后端在Windows的典型实现可能是Windows NT + IIS + Acess
数据库或者是SQL服务器,IIS和数据库之间通过ASP技术使用ODBC进行连接,达到通过填写表单查询数据的功能;后端在Linux系统的典型实现可能是Linux+ Apache + postgresql,Apache和数据库之间通过PHP3提供的函数进行连接。使用上述方法的缺点是后端关系数据库的引入导致系统整体的性能降低和系统的管理比较繁琐,因为需要不断的进行数据类型的验证和事务的完整性的确认;并且前端用户对数据的控制不够灵活,用户权限的设置一般只能是设置在表一级而不是设置在记录一级。 

    目录服务的推出主要是解决上述数据库中存在的问题。目录与关系数据库相似,是指具有描述性的基于属性的记录集合,但它的数据类型主要是字符型,为了检索的需要添加了BIN(二进制数据)、CIS(忽略大小写)、CES(大小写敏感)、TEL(电话型)等语法(Syntax),而不是关系数据库提供的整数、浮点数、日期、货币等类型,同样也不提供象关系数据库中普遍包含的大量的函数,它主要面向数据的查询服务(查询和修改操作比一般是大于 10:1),不提供事务的回滚(rollback)机制,它的数据修改使用简单的锁定机制实现All-or-Nothing,它的目标是快速响应和大容量查询并且提供多目录服务器的信息复制功能。
LDAP最大的优势是: 
    可以在任何计算机平台上,用很容易获得的而且数目不断增加的LDAP的客户端程序访问LDAP目录。而且也很容易定制应用程序为它加上LDAP的支持。 

    LDAP协议是跨平台的和标准的协议,因此应用程序就不用为LDAP目录放在什么样的服务器上操心了。实际上,LDAP得到了业界的广泛认可,因为它是Internet的标准。产商都很愿意在产品中加入对LDAP的支持,因为他们根本不用考虑另一端(客户端或服务端)是怎么样的。LDAP服务器可以是任何一个开发源代码或商用的LDAP目录服务器(或者还可能是具有LDAP界面的关系型数据库),因为可以用同样的协议、客户端连接软件包和查询命令与LDAP服务器进行交互。与LDAP不同的是,如果软件产商想在软件产品中集成对DBMS的支持,那么通常都要对每一个数据库服务器单独定制。不象很多商用的关系型数据库,你不必为LDAP的每一个客户端连接或许可协议付费 大多数的LDAP服务器安装起来很简单,也容易维护和优化。 

    LDAP服务器可以用“推”或“拉”的方法复制部分或全部数据,例如:可以把数据“推”到远程的办公室,以增加数据的安全性。复制技术是内置在LDAP服务器中的而且很容易配置。如果要在DBMS中使用相同的复制功能,数据库产商就会要你支付额外的费用,而且也很难管理。 

    LDAP允许你根据需要使用ACI(一般都称为ACL或者访问控制列表)控制对数据读和写的权限。例如,设备管理员可以有权改变员工的工作地点和办公室号码,但是不允许改变记录中其它的域。ACI可以根据谁访问数据、访问什么数据、数据存在什么地方以及其它对数据进行访问控制。因为这些都是由LDAP目录服务器完成的,所以不用担心在客户端的应用程序上是否要进行安全检查。

IT168技术文档】 
    JNDI简介: 
    JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。 
    JNDI可访问的现有的目录及服务有: 
    DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol 轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。 
    JNDI优点: 
    包含了大量的命名和目录服务,使用通用接口来访问不同种类的服务; 
    可以同时连接到多个命名或目录服务上; 
    建立起逻辑关联,允许把名称同Java对象或资源关联起来,而不必指导对象或资源的物理ID。 
 
   JNDI程序包: 
    javax.naming:命名操作; 
    javax.naming.directory:目录操作; 
    javax.naming.event:在命名目录服务器中请求事件通知; 
    javax.naming.ldap:提供LDAP支持; 
    javax.naming.spi:允许动态插入不同实现。 
    利用JNDI的命名与服务功能来满足企业级APIs对命名与服务的访问,诸如EJBs、JMS、JDBC 2.0以及IIOP上的RMI通过JNDI来使用CORBA的命名服务。 
    
    JNDI与JDBC: 
    JNDI提供了一种统一的方式,可以用在网络上查找和访问服务。通过指定一个资源名称,该名称对应于数据库或命名服务中的一个纪录,同时返回数据库连接建立所必须的信息。 
    代码示例:
try{ Context cntxt = new InitialContext(); DataSource ds = (DataSource) cntxt.lookup("jdbc/dpt"); } catch(NamingException ne){ ... }

    JNDI与JMS:
消息通信是软件组件或应用程序用来通信的一种方法。JMS就是一种允许应用程序创建、发送、接收、和读取消息的JAVA技术。 
    代码示例:
try{ Properties env = new Properties(); InitialContext inictxt = new InitialContext(env); TopicConnectionFactory connFactory = (TopicConnectionFactory) inictxt.lookup
(
"TTopicConnectionFactory"); ... }
catch(NamingException ne){ ... }

    访问特定目录:举个例子,人是个对象,他有好几个属性,诸如这个人的姓名、电话号码、电子邮件地址、邮政编码等属性。    通过getAttributes()方法 
Attribute attr = directory.getAttributes(personName).get("email"); String email = (String)attr.get();

        通过使用JNDI让客户使用对象的名称或属性来查找对象: 
    foxes = directory.search("o=Wiz,c=US", "sn=Fox", controls); 
    通过使用JNDI来查找诸如打印机、数据库这样的对象,查找打印机的例子: 
Printer printer = (Printer)namespace.lookup(printerName); printer.print(document);
        浏览命名空间: 
NamingEnumeration list = namespace.list("o=Widget, c=US"); while (list.hasMore()) { NameClassPair entry = (NameClassPair)list.next(); display(entry.getName(), entry.getClassName()); }
   

IT168技术文档】 
    目前目录服务器主要有: 
    Mircrosoft的活动目录AD 
    SUN ONE的目录服务器 
    Novell的NDS 
    目录服务器是产品,它看起来就象一个树形的数据库,里面可以存各式各样的信息,形象点说象注册表,不过比注册表更强大,更安全!
    目录服务器实现手段不同,不过都遵守LDAP协议。 
    LDAP称为轻型目录存取协议,现在比较好的版本是V3。主要的目录服务器都支持此协议,所以程序基本可以通用。问题是用什么来唯一标记一种资源?有的服务器用一个id,有的服务器用email地址。这回麻烦了,资源的定义各个服务器不同,LDAP的互操作性比较差劲儿。现在IETF已经意识到这点,正在统一此标准。 
    LDAP数据存在目录信息树里。每条是一个记录项,一个entry,每条有唯一的识别名DN(distinguished)。我们看图:

 
    这张图表示了一个企业的组织层次结构。树根表示企业名,比如 NetScaperoot,下一级表示企业的某个层次的部门,比如dc=jssvc,dc=com,代表笔者的机器jssvc.com。ou表示一个子部门,现在名字是groups,最后用户标识是Zeng。
dc=jssvc,dc=com ou=groups uid=Zeng o=netscaperootuid=Zeng,ou=groups,dc=jssvc,dc=com,o=netscaperoot
上面的名字是某结点到根的路径,它唯一地标识了企业中的某资源,它不可能重名。在ldap中,它称为一个entry。 

    现在我们知道,本章的目的就是要教大家如何利用LDAP服务器来管理企业里的资源信息。这些操作有: 
    连接至服务器 
    访问服务器,接受验证 
    搜索服务器 
    加新记录 
    删除记录 
    修改记录 


    目录服务的具体操作 
     连接LDAP服务器 

    验证并连接LDAP服务器
LDAP服务器存有企业资源的所有信息。显然不是谁都能访问的。众多信息对普通用户而言是只读的,比如文件系统位置,打印机信息,部门信息等。 连接LDAP服务器时安全考虑大概有两层: 
    连接到LDAP服务器进行验证,称为 authenticate。客户程序提供用户名密码,告诉服务器你是谁,你想做什么。
连接之后,用户也并非为所欲为。LDAP使用 access control 来控制哪些是某用户可以做的,哪些是不能的。Access Control List的目的就是这个。大家在SUN ONE服务器的目录旁边经常看见的“ACL”字样就是存取控制列表。
安全级别不同,验证强度不同。LDAP版本3提供了以下几种验证方法: 
    -anonymous 匿名方式,不用用户名密码 
    -simple 简单方式,用明码提供用户密码。马马虎虎的场合用。 
    -Simple Authentication and Security Layer (RFC 2222).SASL。这是一个规范,只要ldap和客户端达成协议,可以用各种安全模式交换验证信息。比如 
    -Digest MD5(非常常见) 
    -External 
    -kerberos v4/v5(Windows2000里用的) 
    -等等 
    本章篇幅所限,只能说说简单的验证模式。而且SUN ONE也不支持Kerberos,要用AD处理。至于验证成功后安全传输普通数据,请参考 SSL 连接。我有例子程序实现了SSL连接和传输。上课不讲了。 下面看一个简单的验证程序。
//Conn.java: import java.util.Hashtable; import java.util.Enumeration; import javax.naming.*; import javax.naming.directory.*; /** *This example shows how to add attributes in the ldap server by adding an object. *Creates 7 instances of the user object and stores them in the database */ public class ConnOpenLDAP{ public static void main(String args[]){ try { //Hashtable for environmental information Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=Manager,o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "secret");//密码是secret //Get a reference to a directory context DirContext ctx = new InitialDirContext(env); System.out.println(“Connection Successful!”); } catch(Exception e) { e.printStackTrace(); System.exit(1); } }

IT168技术文档】 
    搜索LDAP服务器 
    搜索有几个要素: 
     ldap对象 
     搜索的开始点 
     搜索的范围 
     搜索查询语句(和SQL类似) 
    ldap对象我们已经取得了。(程序中的ctx) 搜索开始点指的是ldap目录树中某一个结点。比如:
SearchBase=”dc=jssvc,dc=com”或者 SearchBase=”ou=groups,dc=jssvc,dc=com”等。 
    搜索范围指的是搜索结点的所有子树或者是其它范围,具体有子树,本级,下一级, 查询语句象 sql 语句一样,比如 sn=Zeng 或者 cn=lee 等。


    这里我们显示多有得记录,其中包括连接代码:
//连接代码 Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=Manager,o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "secret"); String searchBase= "o=JNDITutorial";//从默认的点开始寻找,这里的根是 dc-dc String searchContents="(ou=*)"; try { /* get a handle to an Initial DirContext */ DirContext ctx = new InitialDirContext(env); /* specify search constraints to search subtree */ SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); /* search for all entries with surname of Jensen */ NamingEnumeration results = ctx.search(searchBase, searchContents, constraints); /* for each entry print out name + all attrs and values */ while (results != null && results.hasMore()) { SearchResult si = (SearchResult)results.next(); /* print its name */ System.out.println("name: " + si.getName()); Attributes attrs = si.getAttributes(); if (attrs == null) { System.out.println("No attributes"); } else { /* print each attribute */ for (NamingEnumeration ae = attrs.getAll(); ae.hasMoreElements();) { Attribute attr = (Attribute)ae.next(); String attrId = attr.getID(); /* print each value */ for (Enumeration vals = attr.getAll(); vals.hasMoreElements(); System.out.println(attrId + ": " + vals.nextElement())) ; } } System.out.println(); } } catch (NamingException e) { System.err.println("Search example failed."); e.printStackTrace(); }
    该运行程序后是搜索出所有用户的所有属性的所有值来,搜索出多少用户取决于你指定的条件 SearchContents,而显示多少属性也是可以指定的。比如你只想显示名字,地址,其它不想显示。

IT168技术文档】 
    搜索并显示有限的属性 
    显示属性必然是显示某条记录的属性。在上例中,取属性集合的语句是 
    Attributes attrs = sr.getAttributes() ; 
    其中 sr 是 Searchresult 类型,代表某条记录,getAttributes 取出它的所有属性。 
    如果要取部分属性,就不用 SearchResult 的 getAttributes 方法,而是用Attributes attrs=ctx.getAttributes(dn,attrArray) ;
这个 getAttributes 方法是 ctx 的。带两个参数: 
    第一个参数 dn,表明你到底要显示哪条记录的属性。一个 dn 唯一地标识了一条记录。 
    用 SearchResult 的 getName 可以取得这条记录的 dn。getName 取得的是相对 dn,相当于文件目录中的相对路径。如果要用绝对路径,可以加上 searchBase 获得全路径。 
    如果你愿意,直接写 dn 串也可以。 
    第二个参数是 attrArray,这是一个字符串数组,每个元素都是一个属性名: 
    String attrArray[]={ “sn”,”street”} 
    就指明,我要取 sn 和 street 两个属性。 
    取得 Attrs 后,它也是个属性集,里面有多个属性,本例中就是 sn 和 Street。每个属性有多个值,所以还要用一循环取属性值:
for(int k=0;k< attrArray.length ;k++){ Attribute item = (Attribute) attrs.get(attrArray[k]);
    属性值集合取到 Attribute 对象中,方法是 get(属性名)。取到之后,Attribute item 当然是一个集合,用循环显示它就得到全部属性值。
程序的代码如下:
Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=Manager,o=JNDITutorial"); env.put(Context.SECURITY_CREDENTIALS, "secret"); String searchBase= "ou=Groups,o=JNDITutorial";//从默认的点开始寻找,这里的根是 dc-dc String searchContents="(cn=*)"; try { /* get a handle to an Initial DirContext */ DirContext ctx = new InitialDirContext(env); /* specify search constraints to search subtree */ SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); /* search for all entries with surname of Jensen */ NamingEnumeration results = ctx.search(searchBase, searchContents, constraints); String attrArray[]={ "ou","cn"}; Enumeration enu; /* for each entry print out name + all attrs and values */ while (results != null && results.hasMore()) { SearchResult si = (SearchResult)results.next(); String dn=si.getName() ; dn+=" , "+searchBase; System.err.println("找到的记录标记是: "+dn ); Attributes attrs=ctx.getAttributes(dn,attrArray) ;//在 imail 里无法正确运行 if (attrs==null){ System.err.println("字段不存在"); } else{ for(int k=0;k< attrArray.length ;k++){ Attribute item = (Attribute) attrs.get(attrArray[k]); System.err.println("属性名:"+attrArray[k]+"的值集是"); enu = item.getAll() ; while(enu!=null && enu.hasMoreElements() ) System.err.print(enu.nextElement()+" ** " ); System.err.println() ; } } System.out.println(); } } catch (NamingException e) { System.err.println("Search example failed."); e.printStackTrace(); }

IT168技术文档】 
    删除记录项 
    这项工作太简单了。 
    第一步:确定要删除记录的 dn 
    第二步:调用 ctx.destroySubcontext(entry). 

    修改指定的属性 
    修改属性当然是指修改某个记录的属性。所以万事之先还是得确定你要修改记录的DN.由于LDAP并不是很擅长做修改,这里修改的操作省略. 

    给 LDAP 添加结点 
    LDAP 服务器里可以存放各种应用程序的数据,特别是对象。一个对象存在LDAP服务器里的形式有三种: 
    形式1:整个对象都存在LDAP服务器里,即使对象有 xxxxGB..:) 
    形式2:存一个引用在LDAP服务器里,真的对象存在别的地方。 
    形式3:把对象的属性存在LDAP服务器里,vb,vc 也能读取。 
    形式1比较爽气,可是占硬盘多。形式2比较省空间,数据库什么都都是用这种方法存在 ldap 里的。形式3比较通用,各种程序都可以读相应的信息。 
    你在 java 里创建好对象用,用 ctx 的 bind 或者 rebind 方法就可以把新对象绑到服务器里了. 

    参考文章: 曾海, 目录服务和JNDI
0
相关文章