不同公司的 LDAP/AD 服务配置各不相同,很难封装一个通用的方法,所以我们在对接 LDAP/AD 的过程中,需要了解自己公司的 LDAP/AD 服务配置是怎么样的,才能写出正确的对接代码,因此下面将拆解过程并提供相关的文档地址。

首先需要了解一些 LDAP/AD 的基本概念:

  • dc:域名的部分,其格式是将完整的域名分成几部分,如域名为 example.com 变成 dc=example,dc=com(一条记录的所属位置)
  • uid:用户ID
  • ou:组织单位,组织单位可以包含其他各种对象(包括其他组织单元)
  • cn:公共名称
  • sn:姓
  • dn:一条记录的位置(唯一)
  • rdn:相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分

Python 对接 LDAP 目前主要有两个库,ldap3python-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,该属性是域用户对象的必需属性
  • 输出
  • result = True 表示用户存在,否则用户不存在

如果上一步的用户查询成功,即结果为 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 服务时,才会遇到了。