Win32:如何验证针对Active Directory的凭据?

Ian*_*oyd 16 windows security authentication winapi active-directory

有人问过,并回答过.NET,但是现在是时候得到原生Win32代码的答案了:

如何验证Windows用户名和密码?

之前问过托管代码这个问题.现在是原生解决方案的时候了.


需要指出一些更常见的解决方案的陷阱:

无效方法1.使用模拟查询Active Directory

很多人建议查询Active Directory.如果抛出异常,那么您知道凭据无效 - 正如此stackoverflow问题中所建议的那样.

然而,这种方法一些严重的缺点:

  • 您不仅要对域帐户进行身份验证,还要进行隐式授权检查.也就是说,您正在使用模拟令牌从AD中读取属性.如果其他有效帐户无权从AD读取,该怎么办?默认情况下,所有用户都具有读取权限,但可以将域策略设置为禁用受限帐户(和/或组)的访问权限.

  • 绑定AD会产生严重的开销,必须在客户端加载AD架构缓存(DirectoryServices使用的ADSI提供程序中的ADSI缓存).这既是网络又是AD服务器,消耗资源 - 而且对于像验证用户帐户这样的简单操作来说太昂贵了.

  • 您依赖于非例外情况的异常失败,并假设这意味着无效的用户名和密码.然后,其他问题(例如,网络故障,AD连接故障,内存分配错误等)被错误地表示为身份验证失败.

使用DirectoryEntry该类是.NET是验证凭据的错误方法的示例:

无效的方法1a - .NET

DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3");
object nativeObject = entry.NativeObject;
Run Code Online (Sandbox Code Playgroud)

无效的方法1b - .NET#2

public static Boolean CheckADUserCredentials(String accountName, String password, String domain)
{
    Boolean result;

    using (DirectoryEntry entry = new DirectoryEntry("LDAP://" + domain, accountName, password))
    {
        using (DirectorySearcher searcher = new DirectorySearcher(entry))
        {
            String filter = String.Format("(&(objectCategory=user)(sAMAccountName={0}))", accountName);
            searcher.Filter = filter;
            try
            {
                SearchResult adsSearchResult = searcher.FindOne();
                result = true;
            }
            catch (DirectoryServicesCOMException ex)
            {
                const int SEC_E_LOGON_DENIED = -2146893044; //0x8009030C;
                if (ex.ExtendedError == SEC_E_LOGON_DENIED)
                {
                    // Failed to authenticate. 
                    result = false;
                }
                else
                {
                    throw;
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

以及通过ADO连接查询Active Directory:

无效的方法1c - 本机查询

connectionString = "Provider=ADsDSOObject;
       User ID=iboyd;Password=Tr0ub4dor&3;
       Encrypt Password=True;Mode=Read;
       Bind Flags=0;ADSI Flag=-2147483648';"

SELECT userAccountControl 
FROM 'LDAP://persuis/DC=stackoverflow,DC=com'
WHERE objectClass='user' and sAMAccountName = 'iboyd'
Run Code Online (Sandbox Code Playgroud)

即使您的凭据有效,这些都会失败,但您无权查看目录条目:

在此输入图像描述

无效的方法2. LogonUser Win32 API

其他人建议使用LogonUser() API函数.这听起来不错,但不幸的是,调用用户有时只需要一个权限,通常只给操作系统本身:

调用LogonUser的进程需要SE_TCB_NAME权限.如果调用进程没有此权限,LogonUser将失败,GetLastError将返回ERROR_PRIVILEGE_NOT_HELD.

在某些情况下,调用LogonUser的进程还必须启用SE_CHANGE_NOTIFY_NAME权限; 否则,LogonUser失败,GetLastError返回ERROR_ACCESS_DENIED.作为管理员组成员的本地系统帐户或帐户不需要此权限.默认情况下,为所有用户启用SE_CHANGE_NOTIFY_NAME,但某些管理员可能会为所有用户禁用它.

省高院" 法案作为操作系统的一部分 " privelage是不是你想要做无可奈何的东西-作为微软在一份指出知识库文章:

...调用LogonUser的进程必须具有SE_TCB_NAME权限(在用户管理器中,这是" 作为操作系统的一部分 "权限).SE_TCB_NAME权限非常强大, 不应授予任何任意用户,以便他们可以运行需要验证凭据的应用程序.

此外,如果指定了空白密码,则对LogonUser()的调用将失败.


有效的.NET 3.5方法 - PrincipalContext

有一种验证方法,仅适用于.NET 3.5及更高版本,允许用户进行身份验证而无需执行授权检查:

// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "stackoverflow.com"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("iboyd", "Tr0ub4dor&3")
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,此代码仅在.NET 3.5及更高版本中可用.

是时候找到原生的等价物了.

Luk*_*uke 9

这是微软的建议.

至于其他答案,我不确定你为什么要击落它们.您在尝试验证凭据时抱怨(相对边缘的情况)失败,但如果您要实际使用这些凭据执行某些操作,那么该操作无论如何都会失败.如果您不打算使用这些凭据实际执行某些操作,那么为什么您需要首先验证它们?这似乎是一个有点人为的情况,但显然我不知道你想要完成什么.