What’s LDAP
Lightweight Directory Access Protocol -> LDAP
直译:轻量级目录访问协议(Directory -> 又可译为电话簿),但是意义不准确
不管是目录还是电话簿,核心点为查找
即 Directory 服务内部的数据组织形式是树形结构,这样做的目的是方便查找。其最开始存在的目的是做白页、黄页类应用,而我们可以利用这个结构做其它事情,比如用一个LDAP服务管理公司内所有地方的用户信息,这样就可以做到单点登录
白页:个人通信目录,这个在上世纪的美国电影中能见到。为什么是白页—以前使用白色纸张记录个人通信目录。黄页也是这个原因。
黄页:企业和团体通信目录,例如80、90后熟知的hao123网址黄页
Protocol Brief Introduction
与LDAP相关的协议很多,但主要是两个系列,这里列出,仅做了解用,感兴趣可以翻看
Series Protocol
X.500
X.500系列协议
目录服务的最初协议,完整地描述了目录、目录服务的组成、目录结构等内容。X.500本身只是一个概览,各部分的实现在其子协议如X.501、X.502中,所以说X.500是一个系列协议。
值得一提的是,常见的X.509公钥证书也属于该系列协议。
LDAP
LDAP并非只有一个协议文件,其涉及到方方面面,每个方面单独由一个协议文件描述,加星号的相对来说很重要
LDAP系列协议
RFC4511*:通过TCP传输消息的方式,规定了传输层面的消息类型、消息内容,如绑定、查询等
RFC4512*:信息模型,规定了schema、object class、attribute type等内容
RFC4513*:规定了鉴权方式和安全机制:TLS、SASL
RFC4514:规定了DN的字符串表示方式
RFC4515:规定了过滤器的格式,过滤器用于查找时指定条件
RFC4516:规定了统一资源定位符的格式,即LDAP端点长啥样
RFC4517*:语法和匹配规则,列举了所有允许的语法和匹配规则
RFC4518:字符串国际化
RFC4519*:应用程序的schema,列举了所有允许的object class和attribute type
RFC2377*:推荐的DIT组织方式,即目录服务的树的组织方式
Abbr
LDAP 总是会有一大堆相关缩写,这里列出常见的
DAP - Directory Access Protocol,即目录访问协议
LDAP - Lightweight Directory Access Protocol,轻量级目录访问协议
DIB - Directory Information Base,目录信息库
DIT - Directory Information Tree,目录信息树
DUA - Directory User Agent,用户代理
DN - Distinguished Name,可区别名称,即唯一名称
RDN - Relative Distinguished Name,相对唯一名称,指DIT内某个节点上的名称,上层节点的DN配合上本节点的RDN,能够构成本节点的DN
c - country name,国家名
cn - common name,通用名称
dc - domain component,域名组件
o - organization name,机构名
ou - organization unit name,机构的单位名
sn - surname,姓
st - state or province name,州或省
Understand X.500
如果不看X.500协议,LDAP中的很多东西是看不懂的。X.500定义了目录服务,LDAP只是它的轻量级实现。
所以如果对 X.500 有一些差不多的认知,LDAP就已经可以理解了,毕竟是阉割版
我们看看 X.500 规定了什么。
定义目录系统
Directory旨在提供一个用户友好的name-address类的映射,其中name不可变,address允许动态变化,即key-value结构。Directory由一批系统组成,每个系统持有对应现实世界的逻辑数据,这些存储的数据叫做DIB,即目录信息库。整体架构如下

一个标准的目录系统,有以下几部分
- 目录服务
- 目录服务的用户
- 访问端点-目录服务用来暴露自己
- 用户和服务之间使用协议如
LDAP
进行交互
- 用户使用LDAP客户端访问目录服务
DIB与DIT
目录信息库,由DIT(目录信息树)、节点Entry、Entry中的属性及属性值构成。
- Entry代表一个节点,类型可以是object,也可以是alias,后者表示一个object entry的别名
- 一个Entry内可以拥有多个属性
- 一个属性内可以拥有一个Type,以及多个Value
下图展示了一个DIT的树形结构:

一个假设的DIT如下

则DN:{C=GB, L=Winslow, O=Graphic Services, CN=Laser Printer}
代表了Laser Printer
。
你可能会想,DIB和常见的数据库有什么关系呢?
实际上他们没什么关系
类似HTTP和TCP。硬要说有关系的话,DIB更加垂直吧🤔,仅适用于这种树形存储结构,查询多、更新少;
数据库则更加通用,相对而言也更加底层。
DIB只是规定了一种存储键值对数据的树形结构,可以用任何方式实现DIB,包括关系型数据库。理论上,HTTP也可以是其它实现,比如QUIC。
目录服务
对目录的一些增删改查操作,看一眼就行
-
读操作
- Read:读指定的entry。LDAP不支持此操作
- Compare:比较给的值和指定entry的值是否一致。这个在验证密码时有用
- List:列出指定entry的所有子entry
- Search:列出满足指定过滤器的所有entry
- Abandon:放弃,作用在一个挂起请求上,标识客户端对该请求不在感兴趣
-
修改操作
- 添加entry
- 移除entry
- 修改entry
- 修改DN,修改某个entry的相对名称。如果该entry有子节点,则子节点的这部分名字也会被修改
-
其它可能的输出
- 错误
- 转移(referral):可能当前服务无法处理这个请求,它会返回一个新的端点,类似HTTP的重定向
分布式Directory

- DSA:Directory System Agent。用来连接服务和DUA。他可以缓存Directory数据,可以用本地数据直接响应,也可以单纯做一个转发
- LDAP Server:是Directory Service的一部分。他可以直接使用本地数据,也可以转发到其它LDAP Server获取数据
其它
协议还规定了目录服务的访问安全、备份等操作,这里暂时忽略。之后值得探究的倒是其安全协议 X.509
LDAP’s Safety Specifications
- 使用TLS安全传输
- 提供简单的匿名和账号密码认证
- 支持SASL构建鉴权和安全服务层
The Practice of LDAP
LDAP目前最常用的用途是做单点登录,在这个场景中 LDAP 一般存储关于用户、用户认证信息、组、用户成员等等,充当一个用户信息的中心仓库,通常使用为用户认证和授权

使用命令行构造LDAP树结构
!!!!
由于该方式过于底层,偏离学习初衷,故命令行方式构造数据树的实验笔者仅做记录
不同版本的 Linux 安装方法差别非常大!
此次实验记录的是快速搭建一个 LDAP 学习和测试环境,没有进行其他高级配置。建议使用虚拟机和全新安装的系统进行操作。
LDIF
LDAP Data Interchange Format,是一个格式化文件
LDAP 数据交换格式文件,它以文本形式存储,用于在服务器之间交换数据。
添加数据以及修改数据都需要通过 LDIF 文件来进行,可以跟关系型数据库的 SQL 文件做类比。
LDIF 文件的格式一般如下:
1
2
3
4
|
dn: <识别名>
<属性 1>: <值 1>
<属性 2>: <值 2>
...
|
LDAP 命令行构造数据
本次涉及软件:
- 操作系统:CentOS 7.4.1708 最小安装(已关闭 SELinux 和防火墙)
- 应用软件:openldap 2.4.44 、phpldapadmin 1.2.3
- CentOS 7 上安装并使用 OpenLDAP
yum install -y openldap-clients openldap-servers
- 创建数据库配置文件
cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG && chown ldap:ldap /var/lib/ldap/DB_CONFIG
- 启动 OpenLDAP 服务
开启:systemctl start slapd
开机自启:systemctl enable slapd
如果没有出现错误,那么 OpenLDAP 的安装就算完成了,不过当前的目录数据库还是空的,下面我们要对数据库进行初始化。
- 设置 OpenLDAP 管理员密码
slappasswd
输入密码,确认后会生成{SSHA}xxxxx
这样的东西,记作passwdA值
- 生成 LDIF 文件
cat << EOF > chrootpw.ldif
然后将下方文本修改对应内容后,去掉所有注释,然后在命令行中粘贴回车
1
2
3
4
5
|
dn: olcDatabase={0}config,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {SSHA}xxxxx #{SSHA}xxxxx 修改为上一步记下来的 passwdA值
EOF
|
- 执行 LDIF 文件
ldapadd -Y EXTERNAL -H ldapi:/// -f chrootpw.ldif
- 导入预设的模式
find /etc/openldap/schema/ -name "*.ldif" -exec ldapadd -Y EXTERNAL -H ldapi:/// -D "cn=config" -f {} \;
接下来要使用命令行来新建根节点
- 生成根节点管理员密码
同上一次的操作,生成后保存为 passwdBBBB值
根节点管理员密码与 OpenLDAP 管理员密码不是同一回事!一个 LDAP 数据库可以包含多个目录树。
- 生成 LDIF 文件
首先请想好一个域名。以 zenandidi.com 为例:
下面根节点的 DN 应该这样写:dc=zenandidi,dc=com
cat << EOF > chdomain.ldif
然后将下方文本修改对应内容后,去掉所有注释,然后在命令行中粘贴回车
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
|
dn: olcDatabase={1}monitor,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
read by dn.base="cn=Manager,dc=xxx,dc=xxx" read by * none #修改 dc=xxx,dc=xxx 为自己的域名
dn: olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcSuffix
olcSuffix: dc=xxx,dc=xxx #修改 dc=xxx,dc=xxx 为自己的域名
dn: olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=Manager,dc=xxx,dc=xxx #修改 dc=xxx,dc=xxx 为自己的域名
dn: olcDatabase={2}hdb,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {SSHA}xxxxx #{SSHA}xxxxx 修改为上一步记下来的值
dn: olcDatabase={2}hdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to attrs=userPassword,shadowLastChange by
dn="cn=Manager,dc=xxx,dc=xxx" write by anonymous auth by self write by * none #修改 dc=xxx,dc=xxx 为自己的域名
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by dn="cn=Manager,dc=xxx,dc=xxx" write by * read #修改 dc=xxx,dc=xxx 为自己的域名
EOF
|
- 执行 LDIF 文件
ldapmodify -Y EXTERNAL -H ldapi:/// -f chdomain.ldif
接下来,开始进行添加用户、组节点
- 生成 LDIF 文件
cat << EOF > basedomain.ldif
然后将下方文本修改对应内容后,去掉所有注释,然后在命令行中粘贴回车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
dn: dc=xxx,dc=xxx #修改 dc=xxx,dc=xxx 为自己的域名
objectClass: top
objectClass: dcObject
objectclass: organization
o: root_ldap
dc: xxx #修改 xxx 为自己域名第一个点左边的内容
dn: cn=Manager,dc=xxx,dc=xxx #修改 dc=xxx,dc=xxx 为自己的域名
objectClass: organizationalRole
cn: Manager
description: Directory Manager
dn: ou=People,dc=xxx,dc=xxx #修改 dc=xxx,dc=xxx 为自己的域名
objectClass: organizationalUnit
ou: People
dn: ou=Group,dc=xxx,dc=xxx #修改 dc=xxx,dc=xxx 为自己的域名
objectClass: organizationalUnit
ou: Group
EOF
|
- 执行 LDIF 文件
请先修改下面命令的dc=xxx,dc=xxx
为自己的域名然后再执行
ldapadd -x -D cn=Manager,dc=xxx,dc=xxx -W -f basedomain.ldif
然后需要输入设定的根节点管理员密码 (生成passwdBBBB
值时输入的密码
- 清理 LDIF 文件
rm basedomain.ldif chdomain.ldif chrootpw.ldif
至此,一个 LDAP 目录树就构建完毕了。
LDAP UI操作
但是 LDAP 的命令行管理工具非常难用,尤其是对于新手来说。
所以,下面我们来安装phpLDAPadmin
这个图形化管理工具,方便新手学习
如果上面的命令行操作有实际操作的话,那么可以跳转到这里
UI创建LDAP环境与数据
进入phpLDAPAdmin
该操作使用docker-compose
一键部署openLDAP
、phpldapadmin
和self-service-password
- 准备
open-ldap
的docker-compose
文件
如下直接给出笔者的实验docker-compose
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
|
version: '2'
services:
openldap:
image: osixia/openldap:1.5.0 ### 如果有私有仓库可以从自己的私有仓库拉取镜像
container_name: openldap
restart: always
environment:
LDAP_LOG_LEVEL: "256"
LDAP_ORGANISATION: "ndzzmc" ### 您的组织名称
LDAP_DOMAIN: "gsymgsym.com" ### 公司域名
LDAP_BASE_DN: "dc=gsymgsym,dc=com" ### 根据域名组成
LDAP_ADMIN_PASSWORD: "Aa123+Bb" ### 密码自己来设置
LDAP_CONFIG_PASSWORD: "Aa123+BbCc"
LDAP_READONLY_USER: "false"
#LDAP_READONLY_USER_USERNAME: "readonly"
#LDAP_READONLY_USER_PASSWORD: "readonly"
LDAP_RFC2307BIS_SCHEMA: "false"
LDAP_BACKEND: "mdb"
#LDAP_TLS: "true"
#LDAP_TLS_CRT_FILENAME: "zaq.test.pem"
#LDAP_TLS_KEY_FILENAME: "zaq.test.key"
#LDAP_TLS_DH_PARAM_FILENAME: "dhparam.pem"
#LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
#LDAP_TLS_ENFORCE: "false"
#LDAP_TLS_CIPHER_SUITE: "SECURE256:-VERS-SSL3.0"
# LDAP_TLS_VERIFY_CLIENT: "demand"
LDAP_REPLICATION: "false"
#LDAP_REPLICATION_CONFIG_SYNCPROV: 'binddn="cn=admin,cn=config" bindmethod=simple credentials="$$LDAP_CONFIG_PASSWORD" searchbase="cn=config" type=refreshAndPersist retry="60 +" timeout=1 starttls=critical'
#LDAP_REPLICATION_DB_SYNCPROV: 'binddn="cn=admin,$$LDAP_BASE_DN" bindmethod=simple credentials="$$LDAP_ADMIN_PASSWORD" searchbase="$$LDAP_BASE_DN" type=refreshAndPersist interval=00:00:00:10 retry="60 +" timeout=1 starttls=critical'
#LDAP_REPLICATION_HOSTS: "#PYTHON2BASH:['ldap://ldap.example.org','ldap://ldap2.example.org']"
KEEP_EXISTING_CONFIG: "false"
LDAP_REMOVE_CONFIG_AFTER_SETUP: "true"
#LDAP_SSL_HELPER_PREFIX: "ldap"
tty: true
stdin_open: true
volumes:
- /opt/openldap/ldap:/var/lib/ldap
- /opt/openldap/slapd.d:/etc/ldap/slapd.d
- /opt/openldap/certs:/container/service/lapd/assets/certs
ports:
- "389:389"
- "636:636"
# For replication to work correctly, domainname and hostname must be
# set correctly so that "hostname"."domainname" equates to the
# fully-qualified domain name for the host.
domainname: "gsymgsym.com"
hostname: "ldap-server"
phpldapadmin:
image: osixia/phpldapadmin:latest
container_name: phpldapadmin
restart: always
environment:
PHPLDAPADMIN_LDAP_HOSTS: "openldap" ### 如果部署后登录不进去有可能是这里出了问题,直接换为部署openldap服务的可达IP试试
PHPLDAPADMIN_HTTPS: "false"
ports:
- "50081:80"
depends_on:
- openldap
self-service-password:
container_name: self-service-password
image: tiredofit/self-service-password:latest
restart: always
ports:
- "50080:80"
environment:
- LDAP_SERVER=ldap://openldap:389
- LDAP_BINDDN=cn=admin,dc=gsymgsym,dc=com
- LDAP_BINDPASS=XXXX
- LDAP_BASE_SEARCH=dc=gsymgsym,dc=com
- MAIL_FROM=it@open.com
- MAIL_FROM_NAME=账号自助服务平台
- SMTP_DEBUG=0
- SMTP_HOST=smtp.qiye.aliyun.com
- SMTP_USER=it@open.com
- SMTP_PASS=jYda52VZ8Ftw1111
- SMTP_PORT=465
- SMTP_SECURE_TYPE=ssl
- SMTP_AUTH_ON=true
- NOTIFY_ON_CHANGE=true
volumes:
- /etc/localtime:/etc/localtime
- /opt/openldap/self-service-password/htdocs:/www/ssp
- /opt/openldap/self-service-password/logs:/www/logs
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
|
- Docker 部署
docker-compose up -d

然后点击Login登录

Login DN 根据docker-compose
就是cn=admin,dc=gsymgsym,dc=com
Password 就是LDAP_ADMIN_PASSWORD
的设置项Aa123+Bb
下图的报错是测试错误密码

正确登录后,如下图

创建数据
这里先在dc下面创建一个ou=group
和一个ou=user
。暂时先不按部分区分
如果想再更细节区分的话,则就先创建一个ou,再在该ou下创建一个ou=group
即可。
创建ou

重复此操作,创建ou=user
,结果如下图

创建 Posix Group
接下来创建Posix Group在group
下

创建 User Account
在ou=user
下创建用户

为用户填写其他属性

已有LDAP环境和数据后的UI登录
- 安装phpLDAPadmin
yum -y install phpldapadmin
- 修改phpLDAPadmin的配置文件
vi /etc/phpldapadmin/config.php
请按实际情况以及注释提示修改以下内容,完成去除 # 号和后面的注释,在命令行窗口中按下 G(大写),然后按下 O(大写),将上面修改的内容直接粘贴到命令行窗口中,再按下 ESC ,最后输入:wq
按回车。
1
2
3
4
5
6
7
8
9
10
|
$servers = new Datastore();
$servers->newServer('ldap_pla');
$servers->setValue('server','name','My LDAP Server');
$servers->setValue('server','host','127.0.0.1');
$servers->setValue('server','port',389);
$servers->setValue('server','base',array('dc=xxx,dc=xxx')); #修改 dc=xxx,dc=xxx 为自己的域名
$servers->setValue('login','auth_type','session');
$servers->setValue('login','bind_id','cn=Manager,dc=xxx,dc=xxx'); #修改 dc=xxx,dc=xxx 为自己的域名
$servers->setValue('login','bind_pass','<密码>'); #填入 5.7.1 设定的根节点管理员密码
$servers->setValue('server','tls',false);
|
- 启动
httpd
服务
启动:systemctl start httpd
开机启动:systemctl enable httpd
- 登录phpLDAPadmin
打开浏览器,访问 http://CentOS7IP/phpldapadmin ,然后按图片中的提示登录即可。

配置完效果如下

LDAP与第三方集成
第三方平台的集成基本上按照操作手册来做,故不做实验
仅做记录
LDAP和JRIA的集成
- 登录进
JRIA
的控制台(需要是管理员身份)
- 点击:配置—>用户管理—>用户目录—>添加目录—>LDAP


补充解释
特别解释几个属性的含义:
LDAP模式
- “基础DN” 填写LDAP的根节点,类似dc=zaq,dc=test;
“附加用户DN” 填写限制用户搜索范围的值,类似ou=people,不填的话从基础DN开始搜索;
“附加组DN” 填写限制用户组搜索范围的值,和上一项类似;
LDAP权限
- 只读:JIRA只能从LDAP中读取用户以及用户组信息,所有对用户及用户组的修改不能通过JIRA进行。
本地只读:相比只读来说,可以在JIRA中添加组,并且会将LDAP同步过来的用户加入到该组中。
读写:不但可以读取LDAP上的用户及组信息,还可以通过JIRA修改这些信息,这些信息会自动同步到LDAP。

LDAP和Confluence的集成
找到gitlab的配置文件:/etc/gitlab/gitlab.rb
,然后修改下面的内容
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
|
### LDAP Settings
###! Docs: https://docs.gitlab.com/omnibus/settings/ldap.html
###! **Be careful not to break the indentation in the ldap_servers block. It is
###! in yaml format and the spaces must be retained. Using tabs will not work.**
gitlab_rails['ldap_enabled'] = true
# gitlab_rails['prevent_ldap_sign_in'] = false
###! **remember to close this block with 'EOS' below**
gitlab_rails['ldap_servers'] = YAML.load <<-'EOS'
main: # 'main' is the GitLab 'provider ID' of this LDAP server
label: 'LDAP'
host: 'xx.xx.xx.xx' ### LDAP服务地址
port: 389
uid: 'uid' ### 指定登录gitlab使用LDAP的哪个字段作为账号
bind_dn: 'cn=admin,dc=zaq,dc=test' ### 这里用自己的管理员账号(需要一个有read权限的账号验证通过后搜索用户输入的用户名是否存在)
password: 'XXX'
encryption: 'plain' # "start_tls" or "simple_tls" or "plain"
# verify_certificates: true
# smartcard_auth: false
active_directory: false ### 如果是 Active Directory LDAP server 则设为true
allow_username_or_email_login: true ### 是否允许email登录
# lowercase_usernames: false ### 是否将用户名转为小写
block_auto_created_users: false ### 是否自动创建用户。如果设置为true则自动注册的账户是被锁定的,需要管理员账户手动的为这些账户解锁,因此此处将其设置为false。当设置为false的时候,就需要保证,对于第三方登录的用户完全可控。
base: 'ou=people,dc=zaq,dc=com' ### 从哪个位置搜索用户,这里填自己的
user_filter: '' ### 表示以某种过滤条件筛选用户,比如我们只希望组为gitlab的用户来访问GitLab,则这里可以设置为:memberOf=ou=gitlab,ou=people,dc=zaq,dc=com
# attributes: # LDAP 中用户的属性
# username: ['uid', 'userid', 'sAMAccountName']
# email: ['mail', 'email', 'userPrincipalName']
# name: 'cn'
# first_name: 'givenName'
# last_name: 'sn'
## EE only
group_base: ''
admin_group: ''
sync_ssh_keys: false
EOS
|
修改完后重启GitLab!
最后最好再取消GitLab的注册功能,新用户只能通过 LDAP 认证的方式进行登陆。
拉取代码时要用LDAP创建的账号密码拉取。
LDAP和Nexus的集成
- 登录管理员账号




LDAP和Harbor的集成
特别注意
在没有添加任何用户之前,你可以修改认证模式(Database模式或者LDAP模式), 但当Harbor系统中已经有至少一个用户之后(除了admin用户外),将不能够修改认证模式。
新版本
新版本的可以直接在页面进行修改,如下:
登录管理员账户,选中配置管理

旧版本
老版本则需要修改harbor.cfg
文件,如下
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
|
##By default the auth mode is db_auth, i.e. the credentials are stored in a local database.
#Set it to ldap_auth if you want to verify a user's credentials against an LDAP server.
auth_mode = ldap_auth
#The url for an ldap endpoint.
ldap_url = xx.xx.xx.xx
#A user's DN who has the permission to search the LDAP/AD server.
#If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.
ldap_searchdn = cn=admin,dc=zaq,dc=test
#the password of the ldap_searchdn
ldap_search_pwd = xxx
#The base DN from which to look up a user in LDAP/AD
ldap_basedn = dc=zaq,dc=com
#Search filter for LDAP/AD, make sure the syntax of the filter is correct.
#ldap_filter = (objectClass=person)
# The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD
ldap_uid = uid
#the scope to search for users, 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE
ldap_scope = 2
#Timeout (in seconds) when connecting to an LDAP Server. The default value (and most reasonable) is 5 seconds.
ldap_timeout = 5
#Verify certificate from LDAP server
ldap_verify_cert = true
#The base dn from which to lookup a group in LDAP/AD
ldap_group_basedn = ou=IT,dc=shileizcc,dc=com
#filter to search LDAP/AD group
ldap_group_filter = objectclass=group
#The attribute used to name a LDAP/AD group, it could be cn, name
ldap_group_gid = cn
#The scope to search for ldap groups. 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE
ldap_group_scope = 2
|
Capture LDAP Data
这里尝试抓取LDAP通讯流量
环境:Java1.8,Docker,Wireshark
使用上方的Dockerfile创建好容器后,使用如下java代码进行连接
一个 Java Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package com.study.ldapp;
public class LdapUser {
public String cn;
public String sn;
public String uid;
public String userPassword;
public String displayName;
public String mail;
public String description;
public String uidNumber;
public String gidNumber;
/**忽略get\set方法**/
}
|
一个模拟客户端
注意!!!
如下代码笔者只运行了链接认证部分
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
package com.study.ldapp;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
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 LdapStore {
public static void main(String[] args) throws NamingException {
String url = "ldap://localhost:389/";
String basedn = "dc=gsymgsym,dc=com"; // basedn
String root = "cn=admin,dc=gsymgsym,dc=com"; // 用户
String pwd ="Aa123+Bb"; // pwd
LdapContext ctx=ldapConnect(url,root,pwd);//集团 ldap认证
List<LdapUser> jtlm=readLdap(ctx,basedn);//获取集团ldap中用户信息
url = "ldap://localhost:389/";
basedn = "dc=gsymgsym,dc=com"; // basedn
root = "cn=admin,dc=gsymgsym,dc=com"; // 用户
pwd ="Aa123+Bb"; // pwd
ctx=ldapConnect(url,root,pwd);//cdh ldap认证
List<LdapUser> cdhlm=readLdap(ctx,basedn);//获取cdh ldap中用户信息
//通过时间戳初始化一个uid
long init_uid= getNowTimeStamp();
//循环集团ldap用户
for(int i=0;i<jtlm.size();i++){
LdapUser jtu=jtlm.get(i);
init_uid=init_uid+i;//生成一个唯一的ID
jtu.setGidNumber(init_uid+"");//设置gid
jtu.setUidNumber(init_uid+"");//设置uid
// if(!"testwx".equals(jtu.getUid()))
// continue;
boolean not_exist =true;
//循环匹配cdh ldap用户
for(int j=0;j<cdhlm.size();j++){
LdapUser cdhu=cdhlm.get(j);
//双方用户存在并且密码相同
if( jtu.getUid().equals(cdhu.getUid()) && jtu.getUserPassword().equals(cdhu.getUserPassword()) ){
not_exist=false;
break;
}else if(jtu.getUid().equals(cdhu.getUid()) && !jtu.getUserPassword().equals(cdhu.getUserPassword())){//双方用户存在,但是密码不同
modifyInformation(jtu,ctx);//修改cdh ldap的密码,改为集团ldap的用户密码
not_exist=false;
break;
}
}
//cdh ldap中没有此用户就添加
if(not_exist){
boolean b = addGoups(jtu,ctx);
if(b)
addUser(jtu,ctx);
}
}
if(ctx!=null)
ctx.close();
}
/**
* 添加组
* @param lu
* @param ctx
* @return
*/
public static boolean addGoups(LdapUser lu,LdapContext ctx) {
BasicAttributes attrsbu = new BasicAttributes();
BasicAttribute objclassSet = new BasicAttribute("objectClass");
objclassSet.add("posixGroup");
objclassSet.add("top");
attrsbu.put(objclassSet);
attrsbu.put("cn", lu.getCn());//显示账号
attrsbu.put("userPassword", "{crypt}x");//显示
attrsbu.put("gidNumber",lu.getGidNumber());/*显示组id */
attrsbu.put("memberUid", lu.getCn());//显示账号
try {
String cn="cn="+lu.getCn()+",ou=Group,dc=tcjf,dc=com";
System.out.println(cn);
ctx.createSubcontext(cn, attrsbu);
System.out.println("添加用户group成功");
return true;
} catch (Exception e) {
System.out.println("添加用户group失败");
e.printStackTrace();
return false;
}
}
/**
* 添加用户
* @param lu
* @param ctx
* @return
*/
public static boolean addUser(LdapUser lu,LdapContext ctx) {
BasicAttributes attrsbu = new BasicAttributes();
BasicAttribute objclassSet = new BasicAttribute("objectClass");
// objclassSet.add("account");
objclassSet.add("posixAccount");
objclassSet.add("inetOrgPerson");
objclassSet.add("top");
objclassSet.add("shadowAccount");
attrsbu.put(objclassSet);
attrsbu.put("uid", lu.getUid());//显示账号
attrsbu.put("sn", lu.getSn());//显示姓名
attrsbu.put("cn", lu.getCn());//显示账号
attrsbu.put("gecos", lu.getCn());//显示账号
attrsbu.put("userPassword", lu.getUserPassword());//显示密码
attrsbu.put("displayName", lu.getDisplayName());//显示描述
attrsbu.put("mail", lu.getMail());//显示邮箱
attrsbu.put("homeDirectory", "/home/" + lu.getCn());//显示home地址
attrsbu.put("loginShell", "/bin/bash");//显示shell方式
attrsbu.put("uidNumber", lu.getUidNumber());/*显示id */
attrsbu.put("gidNumber", lu.getGidNumber());/*显示组id */
try {
String dn="uid="+lu.getCn()+",ou=People,dc=tcjf,dc=com";
System.out.println(dn);
ctx.createSubcontext(dn, attrsbu);
System.out.println("添加用户成功");
return true;
} catch (Exception e) {
System.out.println("添加用户失败");
e.printStackTrace();
return false;
}
}
/**
* 修改属性
* @param lu
* @param ctx
* @return
*/
public static boolean modifyInformation(LdapUser lu,LdapContext ctx) {
try {
ModificationItem[] mods = new ModificationItem[1];
String dn="uid="+lu.getCn()+",ou=People,dc=tcjf,dc=com";
/*添加属性*/
// Attribute attr0 = new BasicAttribute("description", "测试");
// mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE,attr0);
/*修改属性*/
Attribute attr0 = new BasicAttribute("userPassword", lu.getUserPassword());
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr0);
/*删除属性*/
// Attribute attr0 = new BasicAttribute("description", "测试");
// mods[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr0);
ctx.modifyAttributes(dn, mods);
System.out.println("修改成功");
return true;
} catch (NamingException ne) {
System.out.println("修改失败");
ne.printStackTrace();
return false;
}
}
/**
* 删除
* @param dn
* @param ctx
* @return
*/
public static boolean delete(String dn,LdapContext ctx) {
try {
ctx.destroySubcontext(dn);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取ldap认证
* @param url
* @param root
* @param pwd
* @return
*/
public static LdapContext ldapConnect(String url,String root,String pwd){
String factory = "com.sun.jndi.ldap.LdapCtxFactory";
String simple="simple";
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,factory);
env.put(Context.PROVIDER_URL, url);
env.put(Context.SECURITY_AUTHENTICATION, simple);
env.put(Context.SECURITY_PRINCIPAL, root);
env.put(Context.SECURITY_CREDENTIALS, pwd);
LdapContext ctx = null;
Control[] connCtls = null;
try {
ctx = new InitialLdapContext(env, connCtls);
System.out.println( "认证成功:"+url);
}catch (javax.naming.AuthenticationException e) {
System.out.println("认证失败:");
e.printStackTrace();
} catch (Exception e) {
System.out.println("认证出错:");
e.printStackTrace();
}
return ctx;
}
/**
* 获取用户信息
* @param ctx
* @param basedn
* @return
*/
public static List<LdapUser> readLdap(LdapContext ctx,String basedn){
List<LdapUser> lm=new ArrayList<LdapUser>();
try {
if(ctx!=null){
//过滤条件
String filter = "(&(objectClass=*)(uid=*))";
String[] attrPersonArray = { "uid", "userPassword", "displayName", "cn", "sn", "mail", "description" };
SearchControls searchControls = new SearchControls();//搜索控件
searchControls.setSearchScope(2);//搜索范围
searchControls.setReturningAttributes(attrPersonArray);
//1.要搜索的上下文或对象的名称;2.过滤条件,可为null,默认搜索所有信息;3.搜索控件,可为null,使用默认的搜索控件
NamingEnumeration<SearchResult> answer = ctx.search(basedn, filter.toString(),searchControls);
while (answer.hasMore()) {
SearchResult result = (SearchResult) answer.next();
NamingEnumeration<? extends Attribute> attrs = result.getAttributes().getAll();
LdapUser lu=new LdapUser();
while (attrs.hasMore()) {
Attribute attr = (Attribute) attrs.next();
if("userPassword".equals(attr.getID())){
Object value = attr.get();
lu.setUserPassword(new String((byte [])value));
}else if("uid".equals(attr.getID())){
lu.setUid(attr.get().toString());
}else if("displayName".equals(attr.getID())){
lu.setDisplayName(attr.get().toString());
}else if("cn".equals(attr.getID())){
lu.setCn(attr.get().toString());
}else if("sn".equals(attr.getID())){
lu.setSn(attr.get().toString());
}else if("mail".equals(attr.getID())){
lu.setMail(attr.get().toString());
}else if("description".equals(attr.getID())){
lu.setDescription(attr.get().toString());
}
}
if(lu.getUid()!=null)
lm.add(lu);
}
}
}catch (Exception e) {
System.out.println("获取用户信息异常:");
e.printStackTrace();
}
return lm;
}
/**
* string 转 数值
* @param m
* @return
*/
public static String strToint(String m){
if(m==null || "".equals(m))
return "-1";
char [] a=m.toCharArray();
StringBuffer sbu = new StringBuffer();
for(char c:a)
sbu.append((int)c);
System.out.println(sbu.toString());
return sbu.toString();
}
/**
* 获取时间戳
* @return
*/
public static long getNowTimeStamp() {
long time = System.currentTimeMillis();
time = time / 1000;
return time;
}
}
|
使用Wireshark监听本地回环流量,指定tcp.port=389

找到协议为LDAP
的第一个流量,右键该项->跟踪流->TCP Stream,得到如下

接下来就可以根据兴趣去研究LDAP协议了
如下是我流量包的base64,感兴趣的可以拿来解码后保存为.pcap
文件研究
解压方法:echo "1M....+SiYAAA="|base64 -d > LDAP.pcap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
1MOyoQIABAAAAAAAAAAAAAAABAAAAAAAa8RFZ/PgCAA4AAAAOAAAAAIAAABFAAA0TwRAAIAGAAB/
AAABfwAAAQi1AYXkU/U2AAAAAIAC//+TJQAAAgT/1wEDAwgBAQQCa8RFZyfhCAA4AAAAOAAAAAIA
AABFAAA0TwdAAIAGAAB/AAABfwAAAQGFCLUCKHpG5FP1N4AS//8WpgAAAgT/1wEDAwgBAQQCa8RF
Z0fhCAAsAAAALAAAAAIAAABFAAAoTwhAAIAGAAB/AAABfwAAAQi1AYXkU/U3Aih6R1AQJ/kppAAA
a8RFZyvjCABdAAAAXQAAAAIAAABFAABZTwlAAIAGAAB/AAABfwAAAQi1AYXkU/U3Aih6R1AYJ/l9
MgAAMC8CAQFgKgIBAwQbY249YWRtaW4sZGM9Z3N5bWdzeW0sZGM9Y29tgAhBYTEyMytCYmvERWdG
4wgALAAAACwAAAACAAAARQAAKE8KQACABgAAfwAAAX8AAAEBhQi1Aih6R+RT9WhQECf5KXMAAGvE
RWex5wgAOgAAADoAAAACAAAARQAANk8LQACABgAAfwAAAX8AAAEBhQi1Aih6R+RT9WhQGCf55eQA
ADAMAgEBYQcKAQAEAAQAa8RFZ8rnCAAsAAAALAAAAAIAAABFAAAoTwxAAIAGAAB/AAABfwAAAQi1
AYXkU/VoAih6VVAQJ/kpZQAAa8RFZ0PqCADFAAAAxQAAAAIAAABFAADBTw1AAIAGAAB/AAABfwAA
AQi1AYXkU/VoAih6VVAYJ/kf0AAAMIGWAgECY3QEEmRjPWdzeW1nc3ltLGRjPWNvbQoBAgoBAwIB
AAIBAAEBAKAShwtvYmplY3RDbGFzc4cDdWlkMDsEA3VpZAQMdXNlclBhc3N3b3JkBAtkaXNwbGF5
TmFtZQQCY24EAnNuBARtYWlsBAtkZXNjcmlwdGlvbqAbMBkEFzIuMTYuODQwLjEuMTEzNzMwLjMu
NC4ya8RFZ1/qCAAsAAAALAAAAAIAAABFAAAoTw5AAIAGAAB/AAABfwAAAQGFCLUCKHpV5FP2AVAQ
J/gozQAAa8RFZ7ftCAA6AAAAOgAAAAIAAABFAAA2Tw9AAIAGAAB/AAABfwAAAQGFCLUCKHpV5FP2
AVAYJ/jkOgAAMAwCAQJlBwoBAAQABABrxEVn2O0IACwAAAAsAAAAAgAAAEUAAChPEEAAgAYAAH8A
AAF/AAABCLUBheRT9gECKHpjUBAn+Si+AABrxEVny+4IAFAAAABQAAAAAgAAAEUAAExPEUAAgAYA
AH8AAAF/AAABCLUBheRT9gECKHpjUBgn+YcEAAAwIgIBA0IAoBswGQQXMi4xNi44NDAuMS4xMTM3
MzAuMy40LjJrxEVn4e4IACwAAAAsAAAAAgAAAEUAAChPEkAAgAYAAH8AAAF/AAABAYUItQIoemPk
U/YlUBAn+CibAABrxEVnQ+8IACwAAAAsAAAAAgAAAEUAAChPE0AAgAYAAH8AAAF/AAABCLUBheRT
9iUCKHpjUBEn+SiZAABrxEVnU+8IACwAAAAsAAAAAgAAAEUAAChPFEAAgAYAAH8AAAF/AAABAYUI
tQIoemPkU/YmUBAn+CiaAABrxEVndvIIACwAAAAsAAAAAgAAAEUAAChPFUAAgAYAAH8AAAF/AAAB
AYUItQIoemPkU/YmUBEn+CiZAABrxEVnnvIIACwAAAAsAAAAAgAAAEUAAChPFkAAgAYAAH8AAAF/
AAABCLUBheRT9iYCKHpkUBAn+SiYAAA=
|
Ref
https://blog.csdn.net/zou8944/article/details/122287398
https://blog.csdn.net/pushiqiang/article/details/119982651
https://www.cnblogs.com/siyunianhua/p/18309700
https://zhuanlan.zhihu.com/p/32732045
https://blog.csdn.net/ysf15609260848/article/details/126002321
https://blog.csdn.net/ysf15609260848/article/details/126002452