LDAP 是一个为查询、浏览和搜索而优化的专业分布式数据库,它成树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。 目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。 目录服务是由目录数据库和一套访问协议组成的系统。类似以下的信息适合储存在目录中:
- 企业员工和企业客户之类人员信息;
- 公用证书和安全密钥;
- 邮件地址、网址、IP等电脑信息;
- 电脑配置信息。 …
Ldap 数据模型
每一个系统、协议都会有属于自己的模型,LDAP也不例外,在了解LDAP的基本模型之前我们需要先了解几个LDAP的目录树概念
目录树概念
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目(Entry)。
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)。
- 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来(ObjectClass)。
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性(Attribute)。
基本概念
在浏览LDAP相关文档时经常会遇见一些概念,下面是常见概念的简单解释。
条目 Entry
条目,也叫记录项,是LDAP中最基本的颗粒,就像字典中的词条,或者是数据库中的记录。通常对LDAP的添加、删除、更改、检索都是以条目为基本对象的。
dn
:每一个条目都有一个唯一的标识名(distinguished Name ,DN),如上图中一个 dn:”cn=baby,ou=marketing,ou=people,dc=mydomain,dc=org” 。通过DN的层次型语法结构,可以方便地表示出条目在LDAP树中的位置,通常用于检索。
rdn
:一般指dn逗号最左边的部分,如cn=baby。它与RootDN不同,RootDN通常与RootPW同时出现,特指管理LDAP中信息的最高权限用户。
Base DN
:LDAP目录树的最顶部就是根,也就是所谓的“Base DN”,如”dc=mydomain,dc=org”。
属性 Attribute
每个条目都可以有很多属性(Attribute),比如常见的人都有姓名、地址、电话等属性。每个属性都有名称及对应的值,属性值可以有单个、多个,比如你有多个邮箱。
属性不是随便定义的,需要符合一定的规则,而这个规则可以通过schema制定。比如,如果一个entry没有包含在 inetorgperson 这个 schema 中的 objectClass: inetOrgPerson
,那么就不能为它指定employeeNumber属性,因为employeeNumber是在inetOrgPerson中定义的。
LDAP为人员组织机构中常见的对象都设计了属性(比如commonName,surname)。下面有一些常用的别名:
属性 | 语法 | 描述 | 值(举例) |
---|---|---|---|
cn | Directory String | 姓名 | sean |
sn | Directory String | 姓 | Chow |
ou | Directory String | 单位(部门)名称 | IT_SECTION |
o | Directory String | 组织(公司)名称 | example |
telephoneNumber | Telephone Number | 电话号码 | 110 |
objectClass | 内置属性 | organizationalPerson |
对象类 ObjectClass
对象类是属性的集合,LDAP预想了很多人员组织机构中常见的对象,并将其封装成对象类。比如人员(person
)含有姓(sn
)、名(cn
)、电话(telephoneNumber
)、密码(userPassword
)等属性,单位职工(organizationalPerson
)是人员(person
)的继承类,除了上述属性之外还含有职务(title
)、邮政编码(postalCode
)、通信地址(postalAddress
)等属性。
通过对象类可以方便的定义条目类型。每个条目可以直接继承多个对象类,这样就继承了各种属性。如果2个对象类中有相同的属性,则条目继承后只会保留1个属性。对象类同时也规定了哪些属性是基本信息,必须含有(Must 活Required,必要属性):哪些属性是扩展信息,可以含有(May或Optional,可选属性)。
对象类有三种类型:结构类型(Structural)、抽象类型(Abstract) 和 辅助类型(Auxiliary)。
- 结构类型是最基本的类型,它规定了对象实体的基本属性,每个条目属于且仅属于一个结构型对象类。
- 抽象类型可以是结构类型或其他抽象类型父类,它将对象属性中共性的部分组织在一起,称为其他类的模板,条目不能直接集成抽象型对象类。
- 辅助类型规定了对象实体的扩展属性。每个条目至少有一个结构性对象类。
下面是 inetOrgPerson
对象类的在 schema
中的定义,可以清楚的看到它的父类SUB和可选属性MAY、必要属性MUST(继承自 organizationalPerson
),关于各属性的语法则在schema中的attributetype定义。
# inetOrgPerson
# The inetOrgPerson represents people who are associated with an
# organization in some way. It is a structural class and is derived
# from the organizationalPerson which is defined in X.521 [X521].
objectclass ( 2.16.840.1.113730.3.2.2
NAME 'inetOrgPerson'
DESC 'RFC2798: Internet Organizational Person'
SUP organizationalPerson
STRUCTURAL
MAY (
audio $ businessCategory $ carLicense $ departmentNumber $
displayName $ employeeNumber $ employeeType $ givenName $
homePhone $ homePostalAddress $ initials $ jpegPhoto $
labeledURI $ mail $ manager $ mobile $ o $ pager $
photo $ roomNumber $ secretary $ uid $ userCertificate $
x500uniqueIdentifier $ preferredLanguage $
userSMIMECertificate $ userPKCS12 )
)
Schema
对象类(ObjectClass
)、属性类型(AttributeType
)、语法(Syntax
)分别约定了条目、属性、值,他们之间的关系如下图所示。
所以这些构成了模式(Schema)——对象类的集合。
条目数据在导入时通常需要接受模式检查,它确保了目录中所有的条目数据结构都是一致的。
schema(一般在/etc/ldap/schema/
目录)在导入时要注意前后顺序。
backend & database
ldap的后台进程是由slapd 接收、响应请求,但实际存储数据、获取数据的操作是由Backends做的,而数据是存放在database中,所以你可以看到往往你可以看到backend和database指令是一样的值如 bdb 。
一个 backend 可以有多个 database instance,但每个 database 的 suffix 和 rootdn 不一样。openldap 2.4版本的模块是动态加载的,所以在使用backend时需要 moduleload back_bdb
指令。
bdb是一个高性能的支持事务和故障恢复的数据库后端,可以满足绝大部分需求。许多旧文档里(包括官方)说建议将bdb作为首选后端服务(primary backend
),但2.4版文档明确说hdb才是被首先推荐使用的,这从 2.4.40 版默认安装后的配置文件里也可以看出。hdb是基于bdb的,但是它通过扩展的索引和缓存技术可以加快数据访问,修改entries会更有效率,有兴趣可以访问上的链接或 slapd.backends
。
另外config是特殊的backend,用来在运行时管理slapd的配置,它只能有一个实例,甚至无需显式在 slapd.conf
中配置。
TLS & SASL
分布式LDAP 是以明文的格式通过网络来发送信息的,包括client访问ldap的密码(当然一般密码已然是二进制的),SSL/TLS 的加密协议就是来保证数据传送的保密性和完整性。
SASL (Simple Authenticaion and Security Layer)简单身份验证安全框架,它能够实现openldap客户端到服务端的用户验证,也是ldapsearch、ldapmodify这些标准客户端工具默认尝试与LDAP服务端认证用户的方式(前提是已经安装好 Cyrus SASL)。SASL有几大工业实现标准:Kerveros V5、DIGEST-MD5、EXTERNAL、PLAIN、LOGIN。
Kerveros V5是里面最复杂的一种,使用GSSAPI机制,必须配置完整的Kerberos V5安全系统,密码不再存放在目录服务器中,每一个dn与Kerberos数据库的主体对应。DIGEST-MD5稍微简单一点,密码通过saslpasswd2生成放在sasldb数据库中,或者将明文hash存到LDAP dn的userPassword中,每一个authid映射成目录服务器的dn,常和SSL配合使用。参考将 LDAP 客户端配置为使用安全性
EXTERNAL一般用于初始化添加schema时使用,如 ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/core.ldif
。
其他
关键字 | 英文全称 | 含义 |
---|---|---|
dc | Domain Component | 域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example,dc=com(一条记录的所属位置) |
uid | User Id | 用户ID songtao.xu(一条记录的ID) |
使用 ldapsearch 查询
要使用 ldapsearch
工具首先要下载 openldap-clients
包
yum -y install openldap-clients
ldapsearch 参数列表
参数 | 用途 |
---|---|
-? | 打印关于使用 ldapsearch 的帮助。 |
-a deref |
指定别名反向引用。 请输入 never、always、search 或 find。 如果不使用此参数,那么缺省值为 never。 |
-A | 只检索属性的名称,而不检索属性的值。 |
-b base dn |
指定用作搜索起始点的专有名称。 使用引号来指定该值,例如:”ou=West,o=Renovations,c=US” 如果要搜索的服务器需要指定搜索起点,则必须使用此参数。 否则此参数是可选的。(可选)将 -s 与 -b 一起使用来确定搜索范围。没有 -s,-b 就会搜索指定为起始点的条目以及该条目的所有后代。 |
-B | 允许打印非 ASCII 值 |
-D bind dn |
指定服务器用于认证您的专有名称。 名称必须与目录中的条目相符,并且必须拥有搜索目录所需的权限。在引号中指定该名称,例如:”cn=Directory Manager,o=Renovations,c=US” 如果不使用此参数,则与服务器的连接是匿名的。 如果服务器不允许匿名连接,那么必须使用 -D。除了 -D,还必须使用 -w 参数来指定与专有名称相关联的密码。 |
-f file |
指定包含要使用的搜索过滤器的文件,例如 -f 过滤器。请将每个搜索过滤器置于单独的一行。Ldapsearch 会对每一行执行一次搜索。 可选择指定过滤模式。 例如,指定 -f 过滤器 cn=%s,并在文件的每一行中输入公共名称的值。 |
-F sep |
在属性名称和值之间打印 sep 而不是等号 (=)。例如,如果读取 ldapsearch 输出的工具希望使用其他的分隔符时,可以使用此参数。 |
-h host name |
指定要连接到的服务器的主机名,例如,-h server.renovations.com。 |
-l timelimit |
指定完成搜索的时间限制(秒)。 如果没有指定此参数或指定的限制为 0,那么搜索就没有时间限制。但是,ldapsearch 的等待时间决不会超过服务器上设置的搜索时间限制。 |
-L | 指定以 LDIF 格式输出。 LDIF 格式使用冒号 (:) 而不是等号 (=) 作为属性描述符。 LDIF 对一次性添加或修改大量目录条目很有帮助。 例如,可以将输出内容导入兼容 LDAP 的目录中。 |
-M | 将参考对象作为普通条目进行管理,以使 ldapsearch 可返回参考条目本身的属性,而不是所参考的条目的属性。 |
-n | 显示如何执行搜索,但不实际执行搜索 |
-p port |
指定服务器使用的端口。 如果没有使用此参数,缺省情况下 ldapsearch 使用 389 端口。 |
-R | 不自动遵循服务器返回的搜索引用。 |
-s scope |
指定使用 -b 参数时的搜索范围: base - 仅搜索使用 -b 参数指定的条目 onelevel - 仅搜索使用 -b 参数指定的条目的直接子条目,但不搜索该条目本身 subtree - 搜索使用 -b 参数指定的条目及其所有后代, 这是使用 -b 而不使用 -s 时的缺省行为。 指定 -b 和 -s 的顺序并不重要。 |
-S attribute |
按指定的属性排序结果。 |
-z sizelimit |
指定返回条目的最大数目。 如果没有指定此参数或指定的限制为 0,那么返回的条目没有数量限制。但是,ldapsearch 返回的条目决不会多于服务器允许的数量。 |
-u | 指定 ldapsearch 以用户友好格式返回专有名称。 |
-v | 指定 ldapsearch 以详尽方式运行。 |
-w password |
指定与 -D 参数一起使用的专有名称的关联密码。 |
-x | 与 -S 一起使用以指定该 LDAP 服务器在先对结果排序再将其返回。如果使用 -S 而不使用 -x,ldapsearch 将对结果排序。 |
使用搜索过滤器
搜索过滤器的语法为:"attribute operator value"
例如,下面的搜索过滤器可以找到所有的特定条目,只要该条目中以 Smith 作为 sn(姓)属性的值:"sn=Smith"
请注意,如果 LDAP 目录(如 Domino® 目录)支持语言标记,那么可以在搜索过滤器中包含语言标记。例如:"givenName;lang-fr=Etienne"
使用布尔运算符的多个搜索过滤器
您可以使用多个搜索过滤器以及布尔运算符。 使用下列语法: "(operator(filter)(filter))"
例如,使用下面的搜索过滤器查找姓为 Browning、位置为 Dallas 的条目。"(&(sn=Browning)(l=Dallas))"
布尔运算符可以嵌套。 例如,使用下面的搜索过滤器在邮件域 MDN 中查找 surname 为 Caneel 或 givenname 为 Alfred 的条目:"(&(maildomain=MDN)(|(sn=caneel)(givenname=alfred)))"
使用ldapsearch实例
- 指定ldapuri进行查询:
ldapsearch -x -h 192.168.31.242 -p 389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
- 指定hostname和端口号进行查询
ldapsearch -x -h 192.168.31.242 -p 389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
- 指定过滤条件:按照dn进行过滤
ldapsearch -x -H ldap://192.168.31.242:389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin "cn=admin"
Java SDK Client 查询方式:
通过 novell研发的jldap产品
<dependency>
<groupId>com.novell.ldap</groupId>
<artifactId>jldap</artifactId>
<version>4.3</version>
</dependency>
public void start() throws LDAPException, UnsupportedEncodingException {
LDAPConnection lc = new LDAPConnection();
lc.connect("########",389); // connect
lc.bind(LDAPConnection.LDAP_V3, "###########", "########".getBytes("UTF8")); // dn - password
// search : base , scope, fileters, attr, name:value。
LDAPSearchResults rs = lc.search("ou=acs,dc=mobvista,dc=com",LDAPConnection.SCOPE_SUB,"uid=tao.dong@mobvista.com",null,false);
int count = 0;
while(rs.hasMore()){
LDAPEntry entry = rs.next();
System.out.println(entry.getDN());
System.out.println(">>>");
Iterator<LDAPAttribute> iterator = entry.getAttributeSet().iterator();
iterator.forEachRemaining(attr->System.out.println(attr.getName() + ":" + attr.getStringValue()));
System.out.println("==============================");
count++;
}
System.out.println("共有 ["+count+"] 条记录。");
}
通过 JDNI 访问 ldap :
DirContext getDirContext() throws NamingException {
// Set up the initial environment for LDAP connectivity
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
com.sun.jndi.ldap.LdapCtxFactory.class.getName());
env.put(Context.PROVIDER_URL, "########");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
// Set up SSL security, if necessary
// if (useSsl) {
// env.put(Context.SECURITY_PROTOCOL, "ssl");
// System.setProperty("javax.net.ssl.keyStore", keystore);
// System.setProperty("javax.net.ssl.keyStorePassword", keystorePass);
// }
env.put(Context.SECURITY_PRINCIPAL, "########");
env.put(Context.SECURITY_CREDENTIALS, "########");
env.put("com.sun.jndi.ldap.connect.timeout", "100000");
env.put("com.sun.jndi.ldap.read.timeout", "100000");
DirContext ctx = new InitialDirContext(env);
return ctx;
}
public void start() throws NamingException {
DirContext c = getDirContext();
SearchControls SEARCH_CONTROLS = new SearchControls();
SEARCH_CONTROLS.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results = c.search("ou=acs,dc=mobvista,dc=com",
"uid={0}", new Object[]{"tao.dong@mobvista.com"}, SEARCH_CONTROLS);
// return empty list if the user can not be found.
if (!results.hasMoreElements()) {
return;
}
SearchResult result = results.nextElement();
String uidAttribute = result.getAttributes().get("uidNumber").get().toString();
String gidAttribute = result.getAttributes().get("gidNumber").get().toString();
System.out.println("uid : " + uidAttribute);
System.out.println("gid : " + gidAttribute);
}
}
参考文档
LDAP服务器的概念和原理简单介绍 - Sean’s Notes - SegmentFault 思否