LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务。目录服务是一种特殊的数据库系统,其专门针对读取,浏览和搜索操作进行了特定的优化。目录一般用来包含描述性的,基于属性的信息并支持精细复杂的过滤能力。目录一般不支持通用数据库针对大量更新操作操作需要的复杂的事务管理或回卷策略。而目录服务的更新则一般都非常简单。这种目录可以存储包括个人信息、web链结、jpeg图像等各种信息。为了访问存储在目录中的信息,就需要使用运行在TCP/IP 之上的访问协议—LDAP。
LDAP目录中的信息是是按照 树型结构组织,具体信息存储在条目(entry)的数据结构中。常见的例子是通讯簿,由以字母顺序排列的名字、地址和电话号码组成。目录服务与关系数据库之间的主要区别在于:二者都允许对存储数据进行访问,只是目录主要用于读取,其查询的效率很高,而关系数据库则是为读写而设计的。也就是目录服务不适于进行频繁的更新,属于典型的分布式结构。
总结:对于查询操作多于更新操作的(认证)系统来说,使用OpenLDAP是一个比关系数据库如MySq、PostgreSQL等更好的选择。
在LDAP的功能模型中定义了一系列利用LDAP协议的操作,主要包含以下4部分:
查询操作:允许查询目录和取得数据, 其查询性能比关系数据库好。更新操作:目录的更新操作没关系数据库方便,更新性能较差,但也同样允许进行添加、删除、修改等操作。
复制操作:前面也提到过,LDAP是一种典型的分布式结构,提供复制操作,可将主服务器的数据的更新复制到设置的从服务器中。
认证和管理操作:允许客户端在目录中识别自己,并且能够控制一个会话的性质。
而本文所要将的OpenLDAP就是一个优秀的开源的LDAP实现。
安装软件非常简单,但在配置过程中遇到了不少坎坷,不是服务启动不成功就是验证不成功。
具体的安装和配置方法网上一大把,但都参差不齐,主要是因为新旧版本的OpenLDAP不同,配置方法有很大的改动。
下面给出网上几个还算靠谱的Linux和Windows两个平台下安装该软件的方法:
1)ubuntu安装LDAP:安装方法靠谱,但配置说的不太清楚,配置注意事项看后面。
2)Ubuntu OpenLDAP Server:官方教程,最值得借鉴,是英文的,这里有中文版的,但没英文的清晰,说的比较简单。
3)Linux下安装openldap:二进制包安装方法,适用于非Ubuntu的Linux系统,稍微有点麻烦,在安装OpenlDAP之前还需要安装Berkeley DB,但配置灵活,可以自定义安装路径什么的。后面的配置也没说清楚,主要看安装方法。
4)Linux服务器部署系列之七—OpenLDAP篇:另一篇较详细的二进制安装方法及配置。
4)Windows下OpenLDAP的安装及使用:介绍了LDAP的一些基础知识和Windows下安装方法。
5)图文介绍openLDAP在windows上的安装配置:比较详细,值得一看。
上面给出的这几个链接虽然还不错,但还是欠缺了些什么?对,就是讲解,网上给出的教程都是手把手教你如何安装和配置,而没有说明版本差异、具体配置的含义及为什么这样配置,如果因为版本或环境差异,你按其方法配置不成功,你也不知道哪里出的问题,因此建议还是先熟悉LDAP的基础知识,配置文件含义然后再试着安装。
下面根据我自己的经验,给出几个安装和配置注意事项,供参考。
疑惑1:细心的人会发现有的教程说要配置主机DNS,添加与LDAP相关的域名,而大部分教程都没有提及这个,那么到底要不要配置呢?
解答:当然需要配置。安装好OpenLDAP后首先需要配置slapd.conf这个文件,其中里面有
suffix "dc=example, dc=com"
这样一句需要自己配置,这两个dc代表什么意思呢?其实dc就是“domainComponent”,也就是域名的组成部分,准确的说是主机域名的后缀组成部分,如果这里的配置与你的主机域名不对应的话,服务一般是启动不了的。那么怎么配置域名呢?Linux和Windows下的配置文件如下:
Linux下:/etc/hosts
Windows下:C:\Windows\System32\drivers\etc\hosts
需要在hosts文件里添加一条域名(如果没配置的话),格式如下:
127.0.1.1 hostname.example.com hostname
比如我的主机名是min,并添加的域名配置是:
127.0.1.1 min.alexia.cn min那么相应的我就需要在slapd.conf里这样配置suffix:
suffix "dc=alexia, dc=cn"
当然这里域名后缀不一定只有两级,也可以是hostname.example.com.cn,然后suffix就应该是“dc=example, dc=com, dc=cn”,这随便你怎么设置了,只要对应就行。
疑惑2:很多版本的slapd.conf里默认都配置了下面两个变量:
modulepath /usr/lib/ldap moduleload back_@BACKEND@
这是什么意思?需要改动吗?
解答:这是数据库database的backend,一般slapd.conf里配置的database都是bdb,也就是Berkeley DB,有的也许是hdb,其实也是Berkeley DB,只是两个不同的存储引擎(就像Mysql有MyISAM和InnoDB两个不同的存储引擎一样)。而modulepath和moduleload指定了动态模块路径及动态装载的后端模块,因为OpenLDAP默认是用Berkeley DB存储数据的,如果你有动态的数据需要装载,那么就需要配置这两个参数,对于一般用户将这两个注释掉即可。
疑惑3:OpenLDAP默认采用Berkeley DB存储数据,那么可以换用其它的关系数据库吗?具体如何配置呢?
解答:当然可以。首先需要明确ldap数据模型来自RDBMS(关系数据库模型),而并没有指定一定是哪个DB,只要是关系数据库都可以作为LDAP的后台,那么你为什么会想用其它的数据库代替自带的Berkeley DB呢?我想可能是性能相关了,对于少量数据你用哪个都可以,但若涉及到稍大点的数据,比如成千上万的用户查询,那么Berkeley DB的性能就不可观了,而且Berkeley DB管理起来也不太方便,毕竟对这个数据库熟悉的人不多,如果能换作我们经常使用的数据库,不仅性能得到提升,管理起来也十分容易,岂不是一举多得。
具体怎么配置了,请参考这篇文章:用postgresql作后台的openldap,以PostgreSQL作为例子进行讲解。
疑惑4:新旧版本的OpenLDAP到底有什么差异呢?
解答:简单一句话就是:旧版本的OpenLDAP配置文件一般是slapd.conf(路径可能是/etc/openldap,也可能是/usr/local/openldap,甚至可能是/usr/share/slapd/,不同版本不同安装不同系统都可能不同,可使用locate slapd.conf进行查找正确的路径),而新版本(我测试的新版本是2.4.31)的OpenLDAP服务运行时并不会读取该配置文件,而是从slapd.d目录(一般与slapd.conf在同一目录下)中读取相关信息,我们需要把该目录下的数据删掉,然后利用我们在slapd.conf里配置的信息重新生成配置数据。这也可能是你启动服务后运行ldap相关命令却出现“ldap_bind: Invalid credentials (49)”错误的主要原因。具体怎么重新生成配置数据请看参考资料。
疑惑5:自定义的ldif数据文件中的objectclass后的domain、top、organizationalUnit、inetOrgPerson等等都是什么意思,可以随便写吗?
解答:存储LDAP配置信息及目录内容的标准文本文件格式是LDIF(LDAP Interchange Format),使用文本文件来格式来存储这些信息是为了方便读取和修改,这也是其它大多数服务配置文件所采取的格式。LDIF文件常用来向目录导入或更改记录信息,这些信息需要按照LDAP中schema的格式进行组织,并会接受schema 的检查,如果不符合其要求的格式将会出现报错信息。因此,ldif文件中的属性都定义在各大schema中,其中objectclass是对象的类属性,不能随便填写,而应与schema中一致。一般slapd.conf文件的头部都包含了这些schema:
include ../etc/openldap/schema/core.schema
include ../etc/openldap/schema/cosine.schema
include ../etc/openldap/schema/inetorgperson.schema
include ../etc/openldap/schema/nis.schema
include ../etc/openldap/schema/krb5-kdc.schema
include ../etc/openldap/schema/RADIUS-LDAPv3.schema
include ../etc/openldap/schema/samba.schema
其中前三个是比较重要的schema,定义了我们所需要的各个类,比如ldif中一般先定义一个根节点,其相应的objectclass一般是domain和top,而根节点下的ou属性即定义组节点(group)的objectclass一般是organizationalUnit,group下可以是group也可以是用户节点,用户节点的objectclass一般是inetOrgPerson。而各个节点的一系列属性如用户节点的uid、mail、userPassword、sn等等都定义在schema中相关的objectclass里,可以自己查找看看。
疑惑6:OpenLDAP认证用户uid时默认是不区分大小写的,也就是“alexia”与“AleXia”是同一个用户,在有些情况下这并不合理,能配置使得认证时能区分大小写吗?
解答:以我目前的经验来看,旧版本的OpenLDAP是可以配置区分大小写的,而新版本的OpenLDAP却配置不了。为什么这么说呢?
这里就涉及到“matching rules”这个概念了,即匹配规则,就是各个属性按什么样的规则进行匹配,比如是否区分大小写、是否进行数字匹配等等,这里有详细的官方匹配规则描述。比如旧版本的core.schema里有下面这样一段:
attributetype ( 0.9.2342.19200300.100.1.1从字面上也可以看出,其中NAME ( 'uid' 'userid' )DESC 'RFC1274: user identifier'EQUALITY caseIgnoreMatchSUBSTR caseIgnoreSubstringsMatchSYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
caseIgnoreMatch
和
caseIgnoreSubstringsMatch
就定义了uid或userid属性匹配时不区分大小写,如果我们将其改为
caseExactMatch
和
caseExactSubstringsMatch
就表示用户uid认证时需要区分大小写,也就是“alexia”与“AleXia”同不同的用户,这很简单,在旧版本的OpenLDAP也行得通。
可是在新版本的OpenLDAP中却不行,新版本的core.schema文件中也包含这样一段:
#attributetype ( 2.16.840.1.113730.3.1.217
# NAME ( 'uid' 'userid' )
# DESC 'RFC1274: user identifier'
# EQUALITY caseIgnoreMatch
# SUBSTR caseIgnoreSubstringsMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
可惜是注释掉的,那我们取消注释然后改属性行不行呢?答案是不行,会报错:Duplicate attributeType: "2.16.840.1.113730.3.1.217”,也就是说该属性已经被定义了,然后我就去包含的所有schema中搜索uid属性的定义,结果却找不到定义,那么为什么还会报这个错误呢?后来一阵搜索,终于在这个帖子“slapd: built-in schema for uidNumber/gidNumber does not have ordering directive”知道了答案,原来新版本的OpenLDAP已经把uid属性定义schema硬编码到了slapd程序中,也就是无法在配置文件中修改了,真是坑!
针对这个问题,我给出两个不太好的解决方案:
我的主要经验也就这些。OpenLDAP也有客户端,如果你配置成功后,可以用客户端或写Java程序进行验证。
OpenLDAP既有图形客户端也有网页客户端。
主要有两个图形客户端:LdapBrowser282 (下载:LdapBrowser282.zip,下载解压后直接双击:lbe.bat 文件即可运行)和LdapAdmin(官方下载),使用都非常简单。
如下是两个客户端的界面,都需要先建立一个链接,填上相应的IP地址、端口和dn配置,然后连接即可获得你配置的数据。
LDAP Browser客户端:
LDAP Admin客户端:
即 phpLDAPadmin,基于PHP的一个web应用,需要配置Apache服务器和PHP,具体的配置方法可参考“phpLDAPadmin 安装配置讲解,通过 Web 端来管理您的 LDAP 服务器”,我比较偷懒,直接使用的PHPnow全套服务,安装成功后大概是下面这样一个界面:
下面借鉴网上资料提供一个简单的认证程序如下:
import java.util.Hashtable; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.Control; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; public class LDAPAuthentication { private final String URL = "ldap://127.0.0.1:389/"; private final String BASEDN = "ou=Tester,dc=alexia,dc=cn"; // 根据自己情况进行修改 private final String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; private LdapContext ctx = null; private final Control[] connCtls = null; private void LDAP_connect() { Hashtable<String, String> env = new Hashtable<String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY); env.put(Context.PROVIDER_URL, URL + BASEDN); env.put(Context.SECURITY_AUTHENTICATION, "simple"); String root = "cn=manager,dc=alexia,dc=cn"; // 根,根据自己情况修改 env.put(Context.SECURITY_PRINCIPAL, root); // 管理员 env.put(Context.SECURITY_CREDENTIALS, "123456"); // 管理员密码 try { ctx = new InitialLdapContext(env, connCtls); System.out.println( "认证成功" ); } catch (javax.naming.AuthenticationException e) { System.out.println("认证失败:"); e.printStackTrace(); } catch (Exception e) { System.out.println("认证出错:"); e.printStackTrace(); } if (ctx != null) { try { ctx.close(); } catch (NamingException e) { e.printStackTrace(); } } } private String getUserDN(String uid) { String userDN = ""; LDAP_connect(); try { SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); NamingEnumeration<SearchResult> en = ctx.search("", "uid=" + uid, constraints); if (en == null || !en.hasMoreElements()) { System.out.println("未找到该用户"); } // maybe more than one element while (en != null && en.hasMoreElements()) { Object obj = en.nextElement(); if (obj instanceof SearchResult) { SearchResult si = (SearchResult) obj; userDN += si.getName(); userDN += "," + BASEDN; } else { System.out.println(obj); } } } catch (Exception e) { System.out.println("查找用户时产生异常。"); e.printStackTrace(); } return userDN; } public boolean authenricate(String UID, String password) { boolean valide = false; String userDN = getUserDN(UID); try { ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); ctx.reconnect(connCtls); System.out.println(userDN + " 验证通过"); valide = true; } catch (AuthenticationException e) { System.out.println(userDN + " 验证失败"); System.out.println(e.toString()); valide = false; } catch (NamingException e) { System.out.println(userDN + " 验证失败"); valide = false; } return valide; } public static void main(String[] args) { LDAPAuthentication ldap = new LDAPAuthentication(); if(ldap.authenricate("gygtest", "jmwang") == true){ System.out.println( "该用户认证成功" ); } } }
LDAP的实现除了OpenLDAP外,还有其它,比如OpenDJ(Open source Directory services for the Java platform),它是一个新的LDAPv3相容目录服务,为Java平台开发,提供了一个高性能的,高度可用和安全的企业管理的身份商店。其简单的安装过程中,结合了Java平台的力量,使OpenDJ简单和最快的目录服务器部署和管理。有兴趣的可以查阅相关资料。
下一篇我会讲解如何在Tomcat中配置LDAP进行用户认证。