根据Active Directory验证用户名和密码?


Answers:


642

如果使用.NET 3.5或更高版本,则可以使用System.DirectoryServices.AccountManagement名称空间并轻松地验证您的凭据:

// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YOURDOMAIN"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("myuser", "mypassword");
}

简单,可靠,最终是100%C#托管代码-您还能要求什么?:-)

在这里阅读所有内容:

更新:

就像在另一个SO问题(及其答案)中概述的那样,此调用存在一个问题,可能会返回True用户的旧密码。只是要注意这种行为,如果发生这种情况,请不要感到惊讶:-)(感谢@MikeGledhill指出这一点!)


36
在我的域中,我必须指定pc.ValidateCredentials(“ myuser”,“ mypassword”,ContextOptions.Negotiate),否则我将获得System.DirectoryServices.Protocols.DirectoryOperationException:服务器无法处理目录请求。
Alex Peck

12
如果密码已过期或帐户被禁用,则ValidateCredentials将返回false。不幸的是,它没有告诉您它为什么返回false(这很遗憾,因为这意味着我不能做明智的事情,例如重定向用户以更改密码)。
克里斯J

64
还要注意“来宾”帐户-如果启用了域级别的来宾帐户,则如果您给它一个不存在的用户,则ValidateCredentials将返回true 。结果,您可能需要调用UserPrinciple.FindByIdentity以查看传入的用户ID是否首先存在。
克里斯J

7
@AlexPeck:之所以必须这样做(像我一样)是因为.NET默认使用以下技术:LDAP + SSL,Kerberos,然后是RPC。我怀疑RPC已在您的网络中关闭(很好!),除非您明确使用,否则NET实际上不会使用Kerberos ContextOptions.Negotiate
Brett Veenstra

5
请注意,如果用户更改了他们的Active Directory密码,则这段代码将继续使用其旧的AD密码愉快地对用户进行身份验证。是的,真的。有一个在这里读到: stackoverflow.com/questions/8949501/...
迈克·格莱德希尔

70

我们在我们的内联网上做到这一点

您必须使用System.DirectoryServices。

这是代码的胆量

using (DirectoryEntry adsEntry = new DirectoryEntry(path, strAccountId, strPassword))
{
    using (DirectorySearcher adsSearcher = new DirectorySearcher(adsEntry))
    {
        //adsSearcher.Filter = "(&(objectClass=user)(objectCategory=person))";
        adsSearcher.Filter = "(sAMAccountName=" + strAccountId + ")";

        try
        {
            SearchResult adsSearchResult = adsSearcher.FindOne();
            bSucceeded = true;

            strAuthenticatedBy = "Active Directory";
            strError = "User has been authenticated by Active Directory.";
        }
        catch (Exception ex)
        {
            // Failed to authenticate. Most likely it is caused by unknown user
            // id or bad strPassword.
            strError = ex.Message;
        }
        finally
        {
            adsEntry.Close();
        }
    }
}

9
您在“路径”中输入了什么?域的名称?服务器的名称?域的LDAP路径?服务器的LDAP路径?
伊恩·博伊德

3
答1:不,我们将其作为Web服务运行,因此可以从主Web应用程序中的多个位置调用它。答案2:路径包含LDAP信息... LDAP:// DC = domainname1,DC = domainname2,DC = com
DiningPhilanderer

3
看来这可能允许LDAP注入。您可能需要确保转义或删除strAccountId中的任何括号
Brain2000

这是否意味着strPassword以纯文本格式存储在LDAP中?
马特·科卡伊

15
应该永远是一个需要显式调用Close()一个using变量。
Nyerguds

62

这里提出的几种解决方案缺乏区分错误的用户名/密码和需要更改的密码的能力。可以通过以下方式完成:

using System;
using System.DirectoryServices.Protocols;
using System.Net;

namespace ProtocolTest
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                LdapConnection connection = new LdapConnection("ldap.fabrikam.com");
                NetworkCredential credential = new NetworkCredential("user", "password");
                connection.Credential = credential;
                connection.Bind();
                Console.WriteLine("logged in");
            }
            catch (LdapException lexc)
            {
                String error = lexc.ServerErrorMessage;
                Console.WriteLine(lexc);
            }
            catch (Exception exc)
            {
                Console.WriteLine(exc);
            }
        }
    }
}

如果用户密码错误,或者用户不存在,则错误将包含

“ 8009030C:LdapErr:DSID-0C0904DC,注释:AcceptSecurityContext错误,数据52e,v1db1”,

如果需要更改用户密码,它将包含

“ 8009030C:LdapErr:DSID-0C0904DC,注释:AcceptSecurityContext错误,数据773,v1db1”

lexc.ServerErrorMessage数据值是Win32错误代码的十六进制表示。这些错误代码与通过调用Win32 LogonUser API调用将返回的错误代码相同。以下列表总结了十六进制和十进制值的一系列常用值:

525 user not found ​(1317)
52e invalid credentials ​(1326)
530 not permitted to logon at this time (1328)
531 not permitted to logon at this workstation (1329)
532 password expired ​(1330)
533 account disabled ​(1331) 
701 account expired ​(1793)
773 user must reset password (1907)
775 user account locked (1909)

2
不幸的是,某些AD安装程序未返回LDAP子错误代码,这意味着该解决方案将无法工作。
索伦MORS

4
别忘了添加一些对项目的引用: System.DirectoryServicesSystem.DirectoryServices.Protocols
TomXP411

3
但是,我的问题是:如何获取LDAP服务器名称?如果要编写可移植的应用程序,则不能指望用户知道或需要在每个网络上提供AD服务器的名称。
TomXP411 2013年

1
我的用户仅限于登录特定的工作站。如何指定要尝试登录的工作站?(例如,workstation1将失败,并显示数据531,工作站2将正常工作)
akohlsmith 2014年

1
我觉得很奇怪,因为我认为您没有得到足够的信誉。这是一种完全托管的方法,无需调用Win32 API即可确定“用户是否必须重置密码”,这显然没有其他答案。这种方法是否存在漏洞,导致其升值率低?嗯...
Lionet Chen

34

使用DirectoryServices的非常简单的解决方案:

using System.DirectoryServices;

//srvr = ldap server, e.g. LDAP://domain.com
//usr = user name
//pwd = user password
public bool IsAuthenticated(string srvr, string usr, string pwd)
{
    bool authenticated = false;

    try
    {
        DirectoryEntry entry = new DirectoryEntry(srvr, usr, pwd);
        object nativeObject = entry.NativeObject;
        authenticated = true;
    }
    catch (DirectoryServicesCOMException cex)
    {
        //not authenticated; reason why is in cex
    }
    catch (Exception ex)
    {
        //not authenticated due to some other exception [this is optional]
    }

    return authenticated;
}

需要NativeObject访问权限才能检测到错误的用户名/密码


4
该代码很糟糕,因为它也在进行授权检查(检查是否允许用户读取活动目录信息)。用户名和密码可以有效,但不允许用户读取信息-并获得异常。换句话说,您可以具有有效的用户名和密码,但仍然会遇到异常。
伊恩·博伊德

2
我实际上正在寻求本等效项PrincipleContext-仅存在于.NET 3.5中。但是,如果您使用的是.NET 3.5或更高版本,则应使用PrincipleContext
Ian Boyd

28

不幸的是,没有“简单”的方法来检查AD上的用户凭据。

到目前为止,使用每种方法,您都可能得到假阴性:用户的信条将是有效的,但是在某些情况下,AD将返回假:

  • 要求用户在下次登录时更改密码。
  • 用户密码已过期。

ActiveDirectory不允许您使用LDAP来确定密码是否由于用户必须更改密码或密码已过期而无效。

若要确定密码更改或密码已过期,可以调用Win32:LogonUser(),并检查以下两个常量的Windows错误代码:

  • ERROR_PASSWORD_MUST_CHANGE = 1907
  • ERROR_PASSWORD_EXPIRED = 1330

1
请问您从哪里获得了Expired和Must_Change的定义?在这里什么也找不到:)
mabstrei 2012年


谢谢。我试图找出我的验证始终返回false的方式。这是因为用户需要更改密码。
Deise Vicentin

22

可能最简单的方法是PInvoke LogonUser Win32 API.eg

http://www.pinvoke.net/default.aspx/advapi32/LogonUser.html

MSDN参考在这里...

http://msdn.microsoft.com/en-us/library/aa378184.aspx

绝对要使用登录类型

LOGON32_LOGON_NETWORK (3)

这只会创建一个轻量级令牌-非常适合AuthN检查。(其他类型可用于建立交互式会话等)


正如@Alan指出的那样,LogonUser API除了System.DirectoryServices调用之外,还具有许多有用的特性。
stephbu

3
@cciotti:不,那是错误的。正确验证某人的最佳方法是在@stephbu写入时使用LogonUserAPI。这篇文章中描述的所有其他方法将无法100%使用。不过请注意,我相信您必须加入域才能调用LogonUser。
艾伦(Alan)2009年

@Alan生成凭据,您必须能够通过提交有效的域帐户来连接到域。但是,我很确定您的计算机不一定是该域的成员。
stephbu

2
LogonUserAPI要求用户将该Act作为操作系统特权的一部分;这不是用户获得的东西,也不是您想要授予组织中每个用户的东西。(msdn.microsoft.com/en-us/library/aa378184(v=vs.85).aspx
伊恩·博伊德

1
根据support.microsoft.com/kb/180548,LogonUser仅需要充当 Windows 2000及更低版本的操作系统的一部分 ...对于Server 2003及更高版本,它看起来很干净。
克里斯J

18

完整的.Net解决方案是使用System.DirectoryServices命名空间中的类。它们允许直接查询AD服务器。这是一个可以执行此操作的小样本:

using (DirectoryEntry entry = new DirectoryEntry())
{
    entry.Username = "here goes the username you want to validate";
    entry.Password = "here goes the password";

    DirectorySearcher searcher = new DirectorySearcher(entry);

    searcher.Filter = "(objectclass=user)";

    try
    {
        searcher.FindOne();
    }
    catch (COMException ex)
    {
        if (ex.ErrorCode == -2147023570)
        {
            // Login or password is incorrect
        }
    }
}

// FindOne() didn't throw, the credentials are correct

此代码使用提供的凭据直接连接到AD服务器。如果凭据无效,则searcher.FindOne()将引发异常。ErrorCode是与“无效的用户名/密码” COM错误相对应的错误代码。

您无需以AD用户身份运行代码。实际上,我成功地使用它从域外的客户端查询AD服务器上的信息!


身份验证类型如何?我认为您在上面的代码中忘记了它。:-)默认情况下,DirectoryEntry.AuthenticationType设置为“安全”吗?该代码不适用于不安全的LDAP(可能是匿名或无)。我对此正确吗?
jerbersoft

降边查询的AD服务器是,你必须允许查询AD服务器。您的凭据可以有效,但是如果您没有查询AD的权限,则会收到错误消息。这就是为什么创建所谓的“ 快速绑定”的原因。您可以在不授权用户执行某项功能的情况下验证凭据。
伊恩·博伊德

2
如果在检查凭据之前由于任何其他原因引发了COMException,这是否不允许任何人通过?
Stefan Paul Noack

11

另一个.NET调用以快速认证LDAP凭据:

using System.DirectoryServices;

using(var DE = new DirectoryEntry(path, username, password)
{
    try
    {
        DE.RefreshCache(); // This will force credentials validation
    }
    catch (COMException ex)
    {
        // Validation failed - handle how you want
    }
}

这是唯一对我有用的解决方案,使用PrincipalContext不适用于我。
丹尼尔(Daniel)

PrincipalContext对于安全的LDAP连接无效(又名LDAPS,使用端口636
Kiquenet

10

尝试使用此代码(注意:报告无法在Windows Server 2000上运行)

#region NTLogonUser
#region Direct OS LogonUser Code
[DllImport( "advapi32.dll")]
private static extern bool LogonUser(String lpszUsername, 
    String lpszDomain, String lpszPassword, int dwLogonType, 
    int dwLogonProvider, out int phToken);

[DllImport("Kernel32.dll")]
private static extern int GetLastError();

public static bool LogOnXP(String sDomain, String sUser, String sPassword)
{
   int token1, ret;
   int attmpts = 0;

   bool LoggedOn = false;

   while (!LoggedOn && attmpts < 2)
   {
      LoggedOn= LogonUser(sUser, sDomain, sPassword, 3, 0, out token1);
      if (LoggedOn) return (true);
      else
      {
         switch (ret = GetLastError())
         {
            case (126): ; 
               if (attmpts++ > 2)
                  throw new LogonException(
                      "Specified module could not be found. error code: " + 
                      ret.ToString());
               break;

            case (1314): 
               throw new LogonException(
                  "Specified module could not be found. error code: " + 
                      ret.ToString());

            case (1326): 
               // edited out based on comment
               //  throw new LogonException(
               //   "Unknown user name or bad password.");
            return false;

            default: 
               throw new LogonException(
                  "Unexpected Logon Failure. Contact Administrator");
              }
          }
       }
   return(false);
}
#endregion Direct Logon Code
#endregion NTLogonUser

除非您需要为“ LogonException”创建自己的自定义异常


不要使用异常处理从方法中返回信息。“未知的用户名或错误的密码”不是例外,这是LogonUser的标准行为。只需返回false。
Treb

是的...这是一个来自旧VB6库的端口...写于2003左右...(首次出现.Net时)
Charles Bretana

如果在Windows 2000上运行该代码将无法正常工作(support.microsoft.com/kb/180548
伊恩·博伊德

1
重新考虑这一点。登录用户的预期行为是为了使用户登录。如果无法执行该任务,则为例外。实际上,该方法应返回void,而不是布尔值。另外,如果您只返回了布尔值,则该方法的使用者无法通知用户失败的原因是什么。
查尔斯·布雷塔纳'18

5

如果您仍然使用.NET 2.0和托管代码,则可以使用以下另一种方法来使用本地帐户和域帐户:

using System;
using System.Collections.Generic;
using System.Text;
using System.Security;
using System.Diagnostics;

static public bool Validate(string domain, string username, string password)
{
    try
    {
        Process proc = new Process();
        proc.StartInfo = new ProcessStartInfo()
        {
            FileName = "no_matter.xyz",
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
            UseShellExecute = false,
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            RedirectStandardInput = true,
            LoadUserProfile = true,
            Domain = String.IsNullOrEmpty(domain) ? "" : domain,
            UserName = username,
            Password = Credentials.ToSecureString(password)
        };
        proc.Start();
        proc.WaitForExit();
    }
    catch (System.ComponentModel.Win32Exception ex)
    {
        switch (ex.NativeErrorCode)
        {
            case 1326: return false;
            case 2: return true;
            default: throw ex;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return false;
}   

与启动脚本的计算机的本地帐户配合使用
eka808

顺便说一句,需要使用此方法来使其能够公开工作。静态静态字符串。SecureString密码= new SecureString(); foreach(PasswordChars中的字符c)Password.AppendChar(c); ProcessStartInfo foo =新的ProcessStartInfo(); foo.Password =密码; 返回foo.Password; }
eka808

相反,无论如何,应该使用SecureString作为密码。WPF PasswordBox支持它。
斯蒂芬·德鲁

5

Windows身份验证可能由于各种原因而失败:错误的用户名或密码,锁定的帐户,过期的密码等等。要区分这些错误,请通过P / Invoke调用LogonUser API函数,并检查错误代码是否返回false

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

public static class Win32Authentication
{
    private class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private SafeTokenHandle() // called by P/Invoke
            : base(true)
        {
        }

        protected override bool ReleaseHandle()
        {
            return CloseHandle(this.handle);
        }
    }

    private enum LogonType : uint
    {
        Network = 3, // LOGON32_LOGON_NETWORK
    }

    private enum LogonProvider : uint
    {
        WinNT50 = 3, // LOGON32_PROVIDER_WINNT50
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr handle);

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool LogonUser(
        string userName, string domain, string password,
        LogonType logonType, LogonProvider logonProvider,
        out SafeTokenHandle token);

    public static void AuthenticateUser(string userName, string password)
    {
        string domain = null;
        string[] parts = userName.Split('\\');
        if (parts.Length == 2)
        {
            domain = parts[0];
            userName = parts[1];
        }

        SafeTokenHandle token;
        if (LogonUser(userName, domain, password, LogonType.Network, LogonProvider.WinNT50, out token))
            token.Dispose();
        else
            throw new Win32Exception(); // calls Marshal.GetLastWin32Error()
    }
}

用法示例:

try
{
    Win32Authentication.AuthenticateUser("EXAMPLE\\user", "P@ssw0rd");
    // Or: Win32Authentication.AuthenticateUser("user@example.com", "P@ssw0rd");
}
catch (Win32Exception ex)
{
    switch (ex.NativeErrorCode)
    {
        case 1326: // ERROR_LOGON_FAILURE (incorrect user name or password)
            // ...
        case 1327: // ERROR_ACCOUNT_RESTRICTION
            // ...
        case 1330: // ERROR_PASSWORD_EXPIRED
            // ...
        case 1331: // ERROR_ACCOUNT_DISABLED
            // ...
        case 1907: // ERROR_PASSWORD_MUST_CHANGE
            // ...
        case 1909: // ERROR_ACCOUNT_LOCKED_OUT
            // ...
        default: // Other
            break;
    }
}

注意:LogonUser需要与您要验证的域建立信任关系。


您能解释一下为什么您的答案比最高投票的答案更好吗?
穆罕默德·阿里

1
@MohammadAli:如果您需要知道凭据验证失败的原因(不正确的凭据,锁定的帐户,过期的密码等),LogonUser API函数将告诉您。相反,PrincipalContext.ValidateCredentials方法(根据对marc_s答案的评论)不会;在所有这些情况下,它都会返回false。另一方面,LogonUser需要与域建立信任关系,但PrincipalContext.ValidateCredentials(我认为)则不需要。
刘铭文

2

我的简单功能

 private bool IsValidActiveDirectoryUser(string activeDirectoryServerDomain, string username, string password)
    {
        try
        {
            DirectoryEntry de = new DirectoryEntry("LDAP://" + activeDirectoryServerDomain, username + "@" + activeDirectoryServerDomain, password, AuthenticationTypes.Secure);
            DirectorySearcher ds = new DirectorySearcher(de);
            ds.FindOne();
            return true;
        }
        catch //(Exception ex)
        {
            return false;
        }
    }

1

这里是我完整的身份验证解决方案,供您参考。

首先,添加以下四个参考

 using System.DirectoryServices;
 using System.DirectoryServices.Protocols;
 using System.DirectoryServices.AccountManagement;
 using System.Net; 

private void AuthUser() { 


      try{
            string Uid = "USER_NAME";
            string Pass = "PASSWORD";
            if (Uid == "")
            {
                MessageBox.Show("Username cannot be null");
            }
            else if (Pass == "")
            {
                MessageBox.Show("Password cannot be null");
            }
            else
            {
                LdapConnection connection = new LdapConnection("YOUR DOMAIN");
                NetworkCredential credential = new NetworkCredential(Uid, Pass);
                connection.Credential = credential;
                connection.Bind();

                // after authenticate Loading user details to data table
                PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
                UserPrincipal user = UserPrincipal.FindByIdentity(ctx, Uid);
                DirectoryEntry up_User = (DirectoryEntry)user.GetUnderlyingObject();
                DirectorySearcher deSearch = new DirectorySearcher(up_User);
                SearchResultCollection results = deSearch.FindAll();
                ResultPropertyCollection rpc = results[0].Properties;
                DataTable dt = new DataTable();
                DataRow toInsert = dt.NewRow();
                dt.Rows.InsertAt(toInsert, 0);

                foreach (string rp in rpc.PropertyNames)
                {
                    if (rpc[rp][0].ToString() != "System.Byte[]")
                    {
                        dt.Columns.Add(rp.ToString(), typeof(System.String));

                        foreach (DataRow row in dt.Rows)
                        {
                            row[rp.ToString()] = rpc[rp][0].ToString();
                        }

                    }  
                }
             //You can load data to grid view and see for reference only
                 dataGridView1.DataSource = dt;


            }
        } //Error Handling part
        catch (LdapException lexc)
        {
            String error = lexc.ServerErrorMessage;
            string pp = error.Substring(76, 4);
            string ppp = pp.Trim();

            if ("52e" == ppp)
            {
                MessageBox.Show("Invalid Username or password, contact ADA Team");
            }
            if ("775​" == ppp)
            {
                MessageBox.Show("User account locked, contact ADA Team");
            }
            if ("525​" == ppp)
            {
                MessageBox.Show("User not found, contact ADA Team");
            }
            if ("530" == ppp)
            {
                MessageBox.Show("Not permitted to logon at this time, contact ADA Team");
            }
            if ("531" == ppp)
            {
                MessageBox.Show("Not permitted to logon at this workstation, contact ADA Team");
            }
            if ("532" == ppp)
            {
                MessageBox.Show("Password expired, contact ADA Team");
            }
            if ("533​" == ppp)
            {
                MessageBox.Show("Account disabled, contact ADA Team");
            }
            if ("533​" == ppp)
            {
                MessageBox.Show("Account disabled, contact ADA Team");
            }



        } //common error handling
        catch (Exception exc)
        {
            MessageBox.Show("Invalid Username or password, contact ADA Team");

        }

        finally {
            tbUID.Text = "";
            tbPass.Text = "";

        }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.