【问题标题】:Active Directory (LDAP) - Check account locked out / Password expiredActive Directory (LDAP) - 检查帐户被锁定/密码过期
【发布时间】:2010-11-26 12:18:55
【问题描述】:

目前我使用以下代码针对某些 AD 对用户进行身份验证:

DirectoryEntry entry = new DirectoryEntry(_path, username, pwd);

try
{
    // Bind to the native AdsObject to force authentication.
    Object obj = entry.NativeObject;

    DirectorySearcher search = new DirectorySearcher(entry) { Filter = "(sAMAccountName=" + username + ")" };
    search.PropertiesToLoad.Add("cn");
    SearchResult result = search.FindOne();
    if (result == null)
    {
        return false;
    }
    // Update the new path to the user in the directory
    _path = result.Path;
    _filterAttribute = (String)result.Properties["cn"][0];
}
catch (Exception ex)
{
    throw new Exception("Error authenticating user. " + ex.Message);
}

这非常适合根据用户名验证密码。

问题在于总是返回一般错误“登录失败:未知用户名或密码错误”。身份验证失败时。

但是,当帐户被锁定时,身份验证也可能失败。

我怎么知道它是否因为被锁定而失败?

我看到一些文章说你可以使用:

Convert.ToBoolean(entry.InvokeGet("IsAccountLocked"))

或者做类似here解释的事情

问题是,每当您尝试访问 DirectoryEntry 上的任何属性时,都会引发相同的错误。

关于如何找到身份验证失败的实际原因的任何其他建议? (帐户被锁定/密码过期等)

我连接的 AD 不一定是 Windows 服务器。

【问题讨论】:

    标签: c# active-directory ldap


    【解决方案1】:

    我知道这个答案晚了几年,但我们遇到了与原始发帖人相同的情况。不幸的是,在我们的环境中,我们不能使用 LogonUser——我们需要一个纯 LDAP 解决方案。事实证明,有一种方法可以从绑定操作中获取扩展错误代码。这有点难看,但它有效:

    catch(DirectoryServicesCOMException exc)
    {
        if((uint)exc.ExtendedError == 0x80090308)
        {
            LDAPErrors errCode = 0;
    
            try
            {
                // Unfortunately, the only place to get the LDAP bind error code is in the "data" field of the 
                // extended error message, which is in this format:
                // 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 52e, v893
                if(!string.IsNullOrEmpty(exc.ExtendedErrorMessage))
                {
                    Match match = Regex.Match(exc.ExtendedErrorMessage, @" data (?<errCode>[0-9A-Fa-f]+),");
                    if(match.Success)
                    {
                        string errCodeHex = match.Groups["errCode"].Value;
                        errCode = (LDAPErrors)Convert.ToInt32(errCodeHex, fromBase: 16);
                    }
                }
            }
            catch { }
    
            switch(errCode)
            {
                case LDAPErrors.ERROR_PASSWORD_EXPIRED:
                case LDAPErrors.ERROR_PASSWORD_MUST_CHANGE:
                    throw new Exception("Your password has expired and must be changed.");
    
                // Add any other special error handling here (account disabled, locked out, etc...).
            }
        }
    
        // If the extended error handling doesn't work out, just throw the original exception.
        throw;
    }
    

    您还需要错误代码的定义(http://www.lifeasbob.com/code/errorcodes.aspx 上还有很多错误代码):

    private enum LDAPErrors
    {
        ERROR_INVALID_PASSWORD = 0x56,
        ERROR_PASSWORD_RESTRICTION = 0x52D,
        ERROR_LOGON_FAILURE = 0x52e,
        ERROR_ACCOUNT_RESTRICTION = 0x52f,
        ERROR_INVALID_LOGON_HOURS = 0x530,
        ERROR_INVALID_WORKSTATION = 0x531,
        ERROR_PASSWORD_EXPIRED = 0x532,
        ERROR_ACCOUNT_DISABLED = 0x533,
        ERROR_ACCOUNT_EXPIRED = 0x701,
        ERROR_PASSWORD_MUST_CHANGE = 0x773,
        ERROR_ACCOUNT_LOCKED_OUT = 0x775,
        ERROR_ENTRY_EXISTS = 0x2071,
    }
    

    我在其他任何地方都找不到此信息——每个人都只是说您应该使用 LogonUser。如果有更好的解决方案,我很想听听。如果没有,我希望这可以帮助其他无法调用 LogonUser 的人。

    【讨论】:

    • 除了你的链接,我还发现这个 MS 参考很方便:support.microsoft.com/en-us/kb/155012
    • 即使帐户被锁定,我仍然只收到 COMException 而不是 DirectoryServicesCOMException。所以这个方法行不通。
    【解决方案2】:

    有点晚了,但我会把它扔掉。

    如果您真的想确定一个帐户未通过身份验证的具体原因(除了密码错误、过期、锁定等原因还有很多),您可以使用 windows API LogonUser。不要被它吓倒 - 它比看起来更容易。您只需调用 LogonUser,如果失败,请查看 Marshal.GetLastWin32Error(),它将为您提供一个返回代码,指示登录失败的(非常)具体原因。

    但是,您将无法在您正在验证的用户的上下文中调用它;您将需要一个特权帐户 - 我认为要求是 SE_TCB_NAME(又名 SeTcbPrivilege) - 一个有权“作为操作系统的一部分”的用户帐户。

    //Your new authenticate code snippet:
            try
            {
                if (!LogonUser(user, domain, pass, LogonTypes.Network, LogonProviders.Default, out token))
                {
                    errorCode = Marshal.GetLastWin32Error();
                    success = false;
                }
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                CloseHandle(token);    
            }            
            success = true;
    

    如果失败,您将获得其中一个返回码(您可以查找更多代码,但这些是重要的:

     //See http://support.microsoft.com/kb/155012
        const int ERROR_PASSWORD_MUST_CHANGE = 1907;
        const int ERROR_LOGON_FAILURE = 1326;
        const int ERROR_ACCOUNT_RESTRICTION = 1327;
        const int ERROR_ACCOUNT_DISABLED = 1331;
        const int ERROR_INVALID_LOGON_HOURS = 1328;
        const int ERROR_NO_LOGON_SERVERS = 1311;
        const int ERROR_INVALID_WORKSTATION = 1329;
        const int ERROR_ACCOUNT_LOCKED_OUT = 1909;      //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
        const int ERROR_ACCOUNT_EXPIRED = 1793;
        const int ERROR_PASSWORD_EXPIRED = 1330;  
    

    其余的只是复制/粘贴以获取要传入的 DLLImports 和值

      //here are enums
        enum LogonTypes : uint
            {
                Interactive = 2,
                Network =3,
                Batch = 4,
                Service = 5,
                Unlock = 7,
                NetworkCleartext = 8,
                NewCredentials = 9
            }
            enum LogonProviders : uint
            {
                Default = 0, // default for platform (use this!)
                WinNT35,     // sends smoke signals to authority
                WinNT40,     // uses NTLM
                WinNT50      // negotiates Kerb or NTLM
            }
    
    //Paste these DLLImports
    
    [DllImport("advapi32.dll", SetLastError = true)]
            static extern bool LogonUser(
             string principal,
             string authority,
             string password,
             LogonTypes logonType,
             LogonProviders logonProvider,
             out IntPtr token);
    
    [DllImport("kernel32.dll", SetLastError = true)]
            static extern bool CloseHandle(IntPtr handle);
    

    【讨论】:

    • 谢谢。我发现针对 Windows 2008 AD Server 进行测试,对于过期但有效的密码,结果将为 ERROR_PASSWORD_MUST_CHANGE,但如果密码已过期且提供的密码无效,则结果将为 ERROR_LOGON_FAILURE
    • 如果您不能使用 LogonUser 并且需要 LDAP 解决方案,请查看我的回答 (stackoverflow.com/a/16796531/1230982)。
    【解决方案3】:

    以下是当密码被锁定(第一个值)与密码未被锁定时(第二个值)时用户更改的 AD LDAP 属性。 badPwdCountlockoutTime 显然是最相关的。我不确定是否必须手动更新 uSNChanged 和 whenChanged。

    $ diff LockedOut.ldif NotLockedOut.ldif:

    < badPwdCount: 3
    > badPwdCount: 0
    
    < lockoutTime: 129144318210315776
    > lockoutTime: 0
    
    < uSNChanged: 8064871
    > uSNChanged: 8065084
    
    < whenChanged: 20100330141028.0Z
    > whenChanged: 20100330141932.0Z
    

    【讨论】:

      【解决方案4】:

      “密码过期”检查相对容易 - 至少在 Windows 上(不确定其他系统如何处理):当“pwdLastSet”的 Int64 值为 0 时,用户将不得不更改他(或她)下次登录时的密码。检查这一点的最简单方法是在您的 DirectorySearcher 中包含此属性:

      DirectorySearcher search = new DirectorySearcher(entry)
            { Filter = "(sAMAccountName=" + username + ")" };
      search.PropertiesToLoad.Add("cn");
      search.PropertiesToLoad.Add("pwdLastSet");
      
      SearchResult result = search.FindOne();
      if (result == null)
      {
          return false;
      }
      
      Int64 pwdLastSetValue = (Int64)result.Properties["pwdLastSet"][0];
      

      至于“帐户被锁定”检查 - 起初这似乎很容易,但实际上并非如此......“userAccountControl”上的“UF_Lockout”标志不能可靠地完成它的工作。

      从 Windows 2003 AD 开始,您可以检查一个新的计算属性:msDS-User-Account-Control-Computed

      给定一个 DirectoryEntry user,你可以这样做:

      string attribName = "msDS-User-Account-Control-Computed";
      user.RefreshCache(new string[] { attribName });
      
      const int UF_LOCKOUT = 0x0010;
      
      int userFlags = (int)user.Properties[attribName].Value;
      
      if(userFlags & UF_LOCKOUT == UF_LOCKOUT) 
      {
         // if this is the case, the account is locked out
      }
      

      如果您可以使用 .NET 3.5,事情就会变得容易得多 - 查看 MSDN article,了解如何使用 System.DirectoryServices.AccountManagement 命名空间处理 .NET 3.5 中的用户和组。例如。你现在在 UserPrincipal 类上有一个属性IsAccountLockedOut,它可靠地告诉你一个帐户是否被锁定。

      希望这会有所帮助!

      马克

      【讨论】:

      • 感谢 marc ... 将尝试一下。 .NET 3.5 中的 System.DirectoryServices.AccountManagement 不是将我限制在 Windows 活动目录中吗?还是它仍然应用相同的 LDAP 主体?
      • 抱歉 - 是的,S.DS.AM 是特定于 Active Directory 的,抱歉。但是在命名空间 System.DirectoryServices.Protocols 中还有一个“低级”LDAP 库,因为 .NET 2.0(我相信)
      • 嗨,Marc,我尝试了这些建议,但一直遇到同样的问题。如果我将用户名/密码传递给 DirectoryEntry 构造函数,我什至无法应用 DirectorySearcher,因为如果帐户被锁定,身份验证将失败。如果我不通过它,我可以进行搜索,但不要访问任何提及的属性。 “pwdLastSet”不存在,并且 user.Properties 始终为空。我想我得把它放在冰上一段时间。
      • @jabezz:好吧,被锁定的用户当然无法登录并检查他的状态 - 你必须让管理员登录并检查该用户的帐户。
      • @marc :您是对的,但是在代码中完成的身份验证可能会导致帐户被锁定。因此,它应该首先向用户显示由于密码无效而登录失败,然后当他尝试再次登录时,向他显示他的帐户已被锁定的消息,他应该联系管理员。否则,如果我们只是说密码无效,他将继续尝试登录。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-28
      相关资源
      最近更新 更多