You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用Python ldap3验证AD中被锁定/密码过期域用户凭据的问题

使用Python ldap3验证AD中被锁定/密码过期域用户凭据的问题

这个问题我之前对接AD认证时也踩过坑!AD的安全设计就是如此:当账号处于锁定或密码过期状态时,无论用户输入的密码正确与否,绑定请求都会返回result:49的错误,仅通过错误信息里的data字段区分具体状态(比如775对应账号锁定,773对应密码过期),但确实没法直接从绑定结果判断密码本身是否正确。

要解决这个问题,我们得换个思路:先用拥有AD读权限的服务账号提前查询用户的账号状态,再结合凭据验证结果分层处理,具体方案如下:

1. 用服务账号预查询用户的AD状态

首先你需要一个拥有读取AD用户属性权限的专用服务账号(比如企业统一的AD服务账户),通过它绑定AD后,查询目标用户的关键属性来判断账号状态:

  • lockoutTime:判断账号是否被锁定(值大于0即为锁定状态)
  • pwdLastSet + maxPwdAge:计算密码是否过期
  • sAMAccountName:确认用户是否存在

用ldap3实现的示例代码:

from ldap3 import Server, Connection, NTLM
from datetime import datetime, timedelta

# 替换为你的AD实际配置
AD_SERVER = "your-ad-domain-controller.example.com"
AD_SERVICE_USER = "YOUR_DOMAIN\\ad-service-account"
AD_SERVICE_PWD = "your-service-account-password"
AD_SEARCH_BASE = "OU=CompanyUsers,DC=example,DC=com"  # 你的用户所在OU路径

def get_ad_user_status(username):
    # 初始化AD连接
    server = Server(AD_SERVER, get_info=True)
    with Connection(server, user=AD_SERVICE_USER, password=AD_SERVICE_PWD, authentication=NTLM) as conn:
        if not conn.bind():
            print(f"服务账号绑定失败: {conn.result}")
            return None

        # 查询目标用户的核心属性
        search_filter = f"(sAMAccountName={username})"
        target_attrs = ["lockoutTime", "pwdLastSet", "maxPwdAge", "distinguishedName"]
        conn.search(search_base=AD_SEARCH_BASE, search_filter=search_filter, attributes=target_attrs)

        # 检查用户是否存在
        if len(conn.entries) == 0:
            return {
                "exists": False,
                "is_locked": False,
                "pwd_expired": False
            }

        user_entry = conn.entries[0]
        status = {"exists": True}

        # 判断账号锁定状态:lockoutTime > 0 表示已锁定
        lockout_time = int(user_entry.lockoutTime.value) if user_entry.lockoutTime.value else 0
        status["is_locked"] = lockout_time > 0

        # 判断密码是否过期
        # 读取AD全局密码最大有效期(转换为秒,AD中存储为负数的100纳秒单位)
        max_pwd_age = conn.server.info.naming_contexts[0].entry_attributes["maxPwdAge"][0]
        max_pwd_age_seconds = int(max_pwd_age) / -10000000

        pwd_last_set = int(user_entry.pwdLastSet.value) if user_entry.pwdLastSet.value else 0
        if pwd_last_set == 0:
            # pwdLastSet为0表示用户必须重置密码(通常是新账号或强制重置场景)
            status["pwd_expired"] = True
        else:
            # 转换AD时间格式(从1601年1月1日开始的100纳秒计数)
            pwd_last_set_dt = datetime(1601, 1, 1) + timedelta(seconds=pwd_last_set / 10000000)
            current_dt = datetime.now()
            # 计算密码是否超过有效期
            status["pwd_expired"] = (current_dt - pwd_last_set_dt).total_seconds() > max_pwd_age_seconds

        return status

2. 结合账号状态与凭据验证分层处理

拿到用户的账号状态后,我们可以分优先级处理不同场景,避免混淆密码错误与账号状态问题:

  • 先判断用户是否存在
  • 再检查账号是否锁定、密码是否过期
  • 最后在账号状态正常的情况下,验证用户凭据的正确性

对应的验证逻辑代码:

def validate_user_credentials(username, password):
    user_status = get_ad_user_status(username)
    if not user_status:
        return {"success": False, "message": "AD服务连接失败,请稍后重试"}
    
    # 优先处理账号状态问题
    if not user_status["exists"]:
        return {"success": False, "message": "用户名不存在"}
    if user_status["is_locked"]:
        return {"success": False, "message": "账号已被锁定,请联系管理员解锁"}
    if user_status["pwd_expired"]:
        return {"success": False, "message": "密码已过期,请重置密码"}
    
    # 账号状态正常,验证用户凭据
    server = Server(AD_SERVER, get_info=True)
    user_principal = f"YOUR_DOMAIN\\{username}"
    with Connection(server, user=user_principal, password=password, authentication=NTLM) as conn:
        if conn.bind():
            return {"success": True, "message": "验证成功"}
        else:
            # 此时绑定失败即为密码错误
            return {"success": False, "message": "用户名或密码错误"}

补充:AD绑定错误的data字段常见含义

给你整理几个高频出现的AD绑定错误data值,方便快速排查:

  • 52e:用户名或密码无效(真正的凭据错误)
  • 775:账号已被锁定
  • 773:用户必须重置密码(密码过期场景)
  • 530:用户登录时间受限
  • 532:密码已过期(另一种触发场景)

为什么不能直接从绑定结果判断密码正确性?

这是AD的安全防护设计:当账号处于锁定/过期状态时,不会暴露密码是否正确的信息,避免攻击者通过多次尝试猜测有效密码。因此我们只能通过预查询账号状态的方式,先排除状态问题,再验证凭据有效性。

备注:内容来源于stack exchange,提问作者sabirrajabov

火山引擎 最新活动