导读

LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务。目录服务是一种特殊的数据库系统,其专门针对读取,浏览和搜索操作进行了特定的优化。目录一般用来包含描述性的,基于属性的信息并支持精细复杂的过滤能力。目录一般不支持通用数据库针对大量更新操作操作需要的复杂的事务管理或回卷策略。而目录服务的更新则一般都非常简单。这种目录可以存储包括个人信息、web链结、jpeg图像等各种信息。为了访问存储在目录中的信息,就需要使用运行在TCP/IP 之上的访问协议—LDAP。

LDAP目录中的信息是是按照树型结构组织,具体信息存储在条目(entry)的数据结构中。常见的例子是通讯簿,由以字母顺序排列的名字、地址和电话号码组成。

目录服务与关系数据库之间的主要区别在于:二者都允许对存储数据进行访问,只是目录主要用于读取,其查询的效率很高,而关系数据库则是为读写而设计的。也就是目录服务不适于进行频繁的更新,属于典型的分布式结构。

总结:对于查询操作多于更新操作的(认证)系统来说,使用OpenLDAP是一个比关系数据库如MySq、PostgreSQL等更好的选择。

LDAP的功能

在LDAP的功能模型中定义了一系列利用LDAP协议的操作,主要包含以下4部分:

查询操作:允许查询目录和取得数据,其查询性能比关系数据库好。

更新操作:目录的更新操作没关系数据库方便,更新性能较差,但也同样允许进行添加、删除、修改等操作。

复制操作:前面也提到过,LDAP是一种典型的分布式结构,提供复制操作,可将主服务器的数据的更新复制到设置的从服务器中。

认证和管理操作:允许客户端在目录中识别自己,并且能够控制一个会话的性质。

而本文所要将的OpenLDAP就是一个优秀的开源的LDAP实现。

OpenLDAP安装配置及疑惑解答

1. 安装和配置OpenLDAP

安装软件非常简单,但在配置过程中遇到了不少坎坷,不是服务启动不成功就是验证不成功。

具体的安装和配置方法网上一大把,但都参差不齐,主要是因为新旧版本的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的基础知识,配置文件含义然后再试着安装。

2. OpenLDAP疑惑解答

下面根据我自己的经验,给出几个安装和配置注意事项,供参考。

疑惑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 caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )

从字面上也可以看出,其中caseIgnoreMatchcaseIgnoreSubstringsMatch就定义了uid或userid属性匹配时不区分大小写,如果我们将其改为caseExactMatchcaseExactSubstringsMatch就表示用户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程序中,也就是无法在配置文件中修改了,真是坑!

针对这个问题,我给出两个不太好的解决方案:

  1. 下载OpenLDAP源码,找到定义uid属性匹配规则的地方,修改它然后重新编译。这个工作量不轻松,热爱研究源码的人可以尝试。
  2. 不 要用uid属性进行认证,我们可以自定义一个与用户一一对应的属性如user-id(不要与已有的属性重复就行),其配置与uid一模一样(即模仿 uid),然后用该属性作为认证的因子,建议重新建一个schema,然后配置好后include进slapd.conf中重启服务即可。具体怎么定义和 配置可以参考这篇文章

我的主要经验也就这些。OpenLDAP也有客户端,如果你配置成功后,可以用客户端或写Java程序进行验证。

OpenLDAP客户端

OpenLDAP既有图形客户端也有网页客户端。

1. 图形客户端

主要有两个图形客户端:LdapBrowser282 (下载:LdapBrowser282.zip,下载解压后直接双击:lbe.bat 文件即可运行)和LdapAdmin(官方下载),使用都非常简单。

如下是两个客户端的界面,都需要先建立一个链接,填上相应的IP地址、端口和dn配置,然后连接即可获得你配置的数据。

LDAP Browser客户端:

LDAP Admin客户端:

2. 网页客户端

即 phpLDAPadmin,基于PHP的一个web应用,需要配置Apache服务器和PHP,具体的配置方法可参考“phpLDAPadmin 安装配置讲解,通过 Web 端来管理您的 LDAP 服务器”,我比较偷懒,直接使用的PHPnow全套服务,安装成功后大概是下面这样一个界面:

使用Java完成LDAP身份验证

下面借鉴网上资料提供一个简单的认证程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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扩展

LDAP的实现除了OpenLDAP外,还有其它,比如OpenDJ(Open source Directory services for the Java platform),它是一个新的LDAPv3相容目录服务,为Java平台开发,提供了一个高性能的,高度可用和安全的企业管理的身份商店。其简单的安 装过程中,结合了Java平台的力量,使OpenDJ简单和最快的目录服务器部署和管理。有兴趣的可以查阅相关资料。

参考资料

OpenLDAP使用疑惑解答及使用Java完成LDAP身份认证的更多相关文章

  1. 配置OpenLDAP,Java操作LDAP,DBC-LDAP进访问

    LDAP快速入门 1. LDAP简介 LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务.目录服务是一种特殊的 ...

  2. JAVA 通过LDAP获取AD域用户及组织信息

    因为工作需求近期做过一个从客户AD域获取数据实现单点登录的功能,在此整理分享. 前提:用户可能有很多系统的情况下,为了方便账号的统一管理使用AD域验证登录,所以不需要我们的系统登录,就需要获取用户的A ...

  3. JAVA操作LDAP总结

    一.LDAP概念 LDAP的全称为Lightweight Directory Access Protocol(轻量级目录访问协议), 基于X.500标准, 支持 TCP/IP. LDAP目录为数据库, ...

  4. JAVA中SSL证书认证通讯

    JAVA中SSL证书认证通讯 SSL通讯服务端 /******************************************************************** * 项目名称 ...

  5. Security - 轻量级Java身份认证、访问控制安全框架

    前言 此框架由小菜独立开发,并且已经在生产环境中运行大约一年时间. 也就是说,Security 框架写出来有一段时间了,但是一直没有公布.开源,经过不断迭代完善,终于算是拿得出手啦~ Security ...

  6. Java 初级软件工程师 认证考试试卷1

    Java 初级软件工程师 认证考试试卷   笔试(A卷)   考试时间150分钟 总分 100分     姓    名_______________________ 身份证号_____________ ...

  7. java web项目war包部署,使用tomcat对指定接口设置身份认证

    先简单说一下需求: 将一个基于springboot2.0开发的java web项目打成war包,通过tomcat部署到一台linux服务器上,项目相关的一些图片等资源也按照一定规则放置在服务器构建好的 ...

  8. java学习中的一些疑惑解答

    一.java中的枚举类型: 在实际编程中,往往存在着这样的"数据集",它们的数值在程序中是稳定的,而且"数据集"中的元素是有限的.例如星期一到星期日七个数据元素 ...

  9. Java语法基础常见疑惑解答

    1. 类是java的最小单位,java的程序必须在类中才能运行 2. java函数加不加static有何不同 java中声明为static的方法称为静态方法或类方法.静态方法可以直接调用静态方法,访问 ...

随机推荐

  1. lazyload懒加载的使用

    1.引用<script src="http://a.tbcdn.cn/apps/baron/js/??lib/tmm/tmm.js,lib/lazyload/lazyload.js?2 ...

  2. 如何写好CSS?(OOCSS\DRY\SMACSS)

    我现在面对的CSS基本上就是一个三头六臂的怪物,一点不夸张,因为真的是三头六臂,同一个样式在同一个element上作用了好几遍,而同一个样式又分散在4,5个class上,优先级有很多层.可以看得出这个 ...

  3. js中cookie操作

    js中操作Cookie的几种常用方法 * cookie中存在域的概念,使用path和domain区分: * 在同一域中的set和del可以操作同一名称的cookie,但不在同一域中的情况下,则set无 ...

  4. jquery.pagination.js分页

    参数说明 参数名 描述 参数值 maxentries 总条目数                           必选参数,整数 items_per_page 每页显示的条目数            ...

  5. unsigned 整型实现无溢出运算

    普通的 int 整型能表示的范围很有限,所以刷题时很多时候不得不用 long long 来存更大的数据.或者找出数列中某个只出现一次(或奇数次)的数(其余的数均出现两次 / 偶数次),用异或运算的经典 ...

  6. vi编辑器简单应用(摘抄)

    摘抄于 vi编辑器的使用 (2) (3) 1 vi编辑器的基本使用 1.1 vi的启动 打开: $ vi example.c 只读打开 $ vi –R example.c 1.2 vi的工作模式 1. ...

  7. Bootstrap强调内容

    在实际项目中,对于一些重要的文本,希望突出强调的部分都会做另外的样式处理.Bootstrap同样对这部分做了一些轻量级的处理. 如果想让一个段落p突出显示,可以通过添加类名“.lead”实现,其作用就 ...

  8. Spring多数据源的配置和使用

    1. 配置多个数据源 最近开发一个数据同步的小功能,需要从A主机的Oracle数据库中把数据同步到B主机的Oracle库中.当然能够用dmp脚本或者SQL脚本是最好,但是对于两边异构的表结构来说,直接 ...

  9. require 和 file_get_contents

    requirerequire_onceincludeinclude_oncecurlfile_get_contents---各种选择的比较 还有这么复杂的说法,怎么办? 在开发过程中发现,用requi ...

  10. SAP 通过屏幕字段查看透明表

    我要查看创建采购订单屏幕上抬头部分付款条件的这个透明表中的字段. 图1. 1.首先准备好MM模块中的常用透明表. 图2. 2.把光标放在字段上,按F1,再点击图中技术信息按钮. 图3. 3.在弹出的技 ...