Python对接LDAP/AD的过程详解
不同公司的 LDAP/AD 服务配置各不相同,很难封装一个通用的方法,所以我们在对接 LDAP/AD 的过程中,需要了解自己公司的 LDAP/AD 服务配置是怎么样的,才能写出正确的对接代码,因此下面将拆解过程并提供相关的文档地址。
首先需要了解一些 LDAP/AD 的基本概念:
- dc:域名的部分,其格式是将完整的域名分成几部分,如域名为 example.com 变成 dc=example,dc=com(一条记录的所属位置)
- uid:用户ID
- ou:组织单位,组织单位可以包含其他各种对象(包括其他组织单元)
- cn:公共名称
- sn:姓
- dn:一条记录的位置(唯一)
- rdn:相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分
Python 对接 LDAP 目前主要有两个库,ldap3 和 python-ldap 库:
库名称 | 实现语言 | 接口风格 |
---|---|---|
ldap3 | 纯Python | 偏向对象 |
python-ldap | 混合C+Python | 偏向过程 |
综上对比,推荐使用 ldap3 实现 LDAP 对接:
pip install ldap3
首先通过 PIP 安装 ldap3 库,并导入相关类到代码中:
from ldap3 import Server, Connection, ALL
通过 LDAP 服务器地址创建一个 LDAP 服务对象:
server = Server('127.0.0.1', get_info=ALL)
print(server)
# Server(host='127.0.0.1', port=389, use_ssl=False, allowed_referral_hosts=[('*', True)], get_info='ALL', mode='IP_V6_PREFERRED')
观察 LDAP 服务对象的输出信息:
输出信息 | 含义 |
---|---|
host='127.0.0.1' | LDAP 服务器 IP 或 URL |
port=389 | 服务端口,默认就是 389 端口 |
use_ssl=False | 是否使用 SSL,如果为 True ,意味需要 建立安全连接
|
allowed_referral_hosts=[('*', True)] | 限定允许请求的主机 |
get_info='ALL' | 是否必须读取服务器架构和服务器特定信息 |
mode='IP_V6_PREFERRED' | 用于解析 DNS 中的 LDAP 服务器名称的双 IP 堆栈行为 |
更详细的配置及其含义可以查看 LDAP 服务对象 (server-object) 文档。
使用 LDAP 服务对象,基于一个公用账号(使用公有账号可以确保服务稳定)建立 LDAP 连接:
conn = Connection(server, user='Domain\\User', password='password', auto_bind=True, raise_exceptions=True)
print(conn)
# Connection(server=Server(host='127.0.0.1', port=389, use_ssl=False, allowed_referral_hosts=[('*', True)], get_info='ALL', mode='IP_V6_PREFERRED'), user='Domain\\User', password='password', auto_bind='NO_TLS', version=3, authentication='SIMPLE', client_strategy='SYNC', auto_referrals=True, check_names=True, read_only=False, lazy=False, raise_exceptions=False, fast_decoder=True, auto_range=True, return_empty_attributes=True, auto_encode=True, auto_escape=True, use_referral_cache=False)
观察 LDAP 连接对象的输出信息:
输出信息 | 含义 |
---|---|
user='Domain\User' | 绑定的用户的帐户 |
password='password' | 绑定的用户密码 |
auto_bind='NO_TLS' | 自动打开并绑定连接 |
version=3 | LDAP 协议版本 |
authentication='SIMPLE' | 身份验证方法 |
client_strategy='SYNC' | 客户端使用的通信策略 |
auto_referrals=True | 指定连接是否服务器中允许的 |
check_names=True | 搜索结果将按照结构中指定的格式进行格式化 |
read_only=False | 为 True 时禁止修改、删除、添加等操作 |
lazy=False | 为 True 时连接将延迟打开和绑定,直到请求另一个 LDAP 操作 |
raise_exceptions=False | 为 True 时引发 LDAPOperationResult 的异常 |
fast_decoder=True | 为 False 时使用 pyasn1 解码器而不是内部解码器 |
更详细的配置及其含义可以查看 LDAP 连接对象 (Connection) 文档。
到这一步的时候,可以询问 LDAP 服务器当前连接用户是谁?简单验证一下连接有效性:
conn.extend.standard.who_am_i()
# 'u:Domain\\User'
使用公用账号查询某个用户的 SAMAccountName
信息是否存在:
result = conn.search(search_base='OU=OU,DC=Domain,DC=LOCAL', search_filter='(sAMAccountName=xiaoming)')
# True
观察 LDAP 连接对象的 search()
函数输入/输出信息:
- 输入
- search_base = 搜索用户的基础路径
- search_filter = 过滤 LDAP 用户的过滤器语句
- sAMAccountName = 用于存储账户登录名或用户符号,实际上是命名符号
Domain\LogonName
,该属性是域用户对象的必需属性
- sAMAccountName = 用于存储账户登录名或用户符号,实际上是命名符号
- 输出
- result =
True
表示用户存在,否则用户不存在
- result =
如果上一步的用户查询成功,即结果为 True
,下面就可以查看其 response
信息,获取查询到的用户详细信息:
conn.response
# [{'raw_dn': b'CN=\xbd...,DC=LOCAL', 'dn': 'CN=小明-10001001,OU=自动化测试组,OU=测试部,OU=研发中心,OU=Domain,OU=行政组织,OU=OU,DC=LEEDARSON,DC=LOCAL', 'raw_attributes': {}, 'attributes': {}, 'type': 'searchResEntry'}]
上面返回的用户 dn 信息有两种:
-
raw_dn
原始信息conn.response[0]['raw_dn'].decode('UTF-8') # 'CN=小明-10001001,OU=自动化测试组,OU=测试部,OU=研发中心,OU=Domain,OU=行政组织,OU=OU,DC=LEEDARSON,DC=LOCAL'
-
dn
可读信息conn.response[0]['dn'] # 'CN=小明-10001001,OU=自动化测试组,OU=测试部,OU=研发中心,OU=Domain,OU=行政组织,OU=OU,DC=LEEDARSON,DC=LOCAL'
不管是那种格式,信息本身的内容是一样的。默认情况下使用 user_dn = conn.response[0]['dn']
获取用户 dn 信息就可以。
接下来就使用用户 dn 信息去验证用户的密码是否正确,如果密码正确,就和前面公用账号登录一样可以获取用户信息。如果登录异常,我们可以根据响应内容判断具体异常的原因:
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
try:
Connection(server, user=user_dn, password='password', auto_bind=True, raise_exceptions=True)
except LDAPInvalidCredentialsResult as e:
if '52e' in e.message:
print('账号密码不正确')
elif '775' in e.message:
print('账号已锁定,请联系管理员或等待自动解锁')
elif '533' in e.message:
print('账号已禁用')
else:
print('认证失败,请联系管理员检查该账号')
更多具体的情况,就需要实际对接公司的 LDAP/AD 服务时,才会遇到了。