代码之家  ›  专栏  ›  技术社区  ›  Jabezz

Active Directory(LDAP)-检查帐户锁定/密码过期

  •  18
  • Jabezz  · 技术社区  · 16 年前

    目前,我使用以下代码对某些广告的用户进行身份验证:

    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上的任何属性时,都会引发相同的错误。

    还有其他关于如何找到认证失败的实际原因的建议吗?(帐户已锁定/密码已过期/等)

    我连接的广告可能不一定是Windows服务器。

    4 回复  |  直到 13 年前
        1
  •  16
  •   takrl cck    13 年前

    有点晚了,但我会把这个扔出去。

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

    但是,您将无法在正在进行身份验证的用户的上下文中调用它;您将需要一个私有帐户-我认为需要的是SE-TCB-U名称(也称为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);
    
        2
  •  7
  •   Rand Scullard    13 年前

    我知道这个答案晚了几年,但我们只是碰到了和原来海报一样的情况。不幸的是,在我们的环境中,我们不能使用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的人。

        3
  •  4
  •   marc_s MisterSmith    16 年前

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

    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 .

    给一个导演 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 命名空间。例如,你现在确实有一个财产 IsAccountLockedOut 在UserPrincipal类上,该类可靠地告诉您帐户是否被锁定。

    希望这有帮助!

    马克

        4
  •  1
  •   joeforker    16 年前

    以下是在锁定密码(第一个值)和未锁定密码(第二个值)时为用户更改的AD LDAP属性。 badPwdCount lockoutTime 显然是最相关的。我不确定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