Win32:如何根据Active Directoryvalidation凭据?

它已被问及 .NET的答案,但现在是时候得到本机Win32代码的答案:

我如何validationWindows用户名和密码?

我之前问过这个问题的托pipe代码 。 现在是本地解决scheme的时候了。


它需要指出一些更常见的解决scheme的陷阱:

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

很多人build议查询Active Directory的东西。 如果抛出exception,那么你知道凭据是无效的 – 正如在这个stackoverflow问题中所build议的。

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

  • 您不仅要validation域帐户,还要进行隐式授权检查。 也就是说,您正在使用模拟令牌从AD读取属性。 如果否则有效的帐户没有权利从AD读取? 默认情况下,所有用户都具有读取访问权限,但可以将域策略设置为禁用受限帐户(和/或组)的访问权限。

  • 针对AD的绑定具有严重开销,AD架构caching必须在客户端(DirectoryServices使用的ADSI提供程序中的ADSIcaching)中加载。 这既是networking,又是AD服务器,消耗资源,而且对于像validation用户帐户这样的简单操作来说太昂贵了。

  • 对于非例外情况,您依赖exception失败,并假设这意味着无效的用户名和密码。 其他问题(如networking故障,AD连接故障,内存分配错误等)则被误解为authentication失败。

.NET的DirectoryEntry类的使用是validation凭据的不正确方法的一个例子:

无效的方法1a – .NET

 DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3"); object nativeObject = entry.NativeObject; 

无效的方法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; } } } } 

以及通过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' 

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

在这里输入图像说明

无效的方法2. LogonUser Win32 API

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

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

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

正如微软在一篇知识库文章中指出的那样,将“ 作为操作系统的一部分”的标准提交给你是不可能的,

…正在调用LogonUser的进程必须具有SE_TCB_NAME特权(在用户pipe理器中,这是“ 作为操作系统的一部分 ”)。 SE_TCB_NAME特权非常强大, 不应该被授予任何任意用户,以便他们可以运行需要validation凭据的应用程序

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


有效的.NET 3.5方法 – PrincipalContext

有一种validation方法,仅在.NET 3.5及更新版本中可用,它允许用户进行身份validation,而不执行授权检查:

 // create a "principal context" - eg 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") } 

不幸的是,这个代码仅适用于.NET 3.5及更高版本。

是时候find原生的等价物了。

Solutions Collecting From Web of "Win32:如何根据Active Directoryvalidation凭据?"

这是微软的建议 。

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

对于有效.NET解决方案的本地平等,请参阅此 MSDN页面和ldap_bind

HOWVER我认为LogonUser是与LOGON32_LOGON_NETWORK使用时的正确API。 请注意, SE_CHANGE_NOTIFY_NAME的限制仅适用于Windows 2000(因此,Windows XP和更新版本不需要此权限),并且默认情况下已为所有用户启用SE_CHANGE_NOTIFY_NAME。 另外MSDN页面说

除非您登录到Passport帐户,否则此功能不需要SE_TCB_NAME权限。

在这种情况下,您正在登录到AD帐户,因此SE_TCB_NAME不是必需的。

我不妨发布本地代码来验证一组Windows凭据。 实施需要一段时间。

 function TSSPLogon.LogonUser(username, password, domain: string; packageName: string='Negotiate'): HRESULT; var ss: SECURITY_STATUS; packageInfo: PSecPkgInfoA; cbMaxToken: DWORD; clientBuf: PByte; serverBuf: PByte; authIdentity: SEC_WINNT_AUTH_IDENTITY; cbOut, cbIn: DWORD; asClient: AUTH_SEQ; asserver: AUTH_SEQ; Done: boolean; begin { If domain is blank will use the current domain. To force validation against the local database use domain "." sspiProviderName is the same of the Security Support Provider Package to use. Some possible choices are: - Negotiate (Preferred) Introduced in Windows 2000 (secur32.dll) Selects Kerberos and if not available, NTLM protocol. Negotiate SSP provides single sign-on capability called as Integrated Windows Authentication. On Windows 7 and later, NEGOExts is introduced which negotiates the use of installed custom SSPs which are supported on the client and server for authentication. - Kerberos Introduced in Windows 2000 and updated in Windows Vista to support AES) (secur32.dll) Preferred for mutual client-server domain authentication in Windows 2000 and later. - NTLM Introduced in Windows NT 3.51 (Msv1_0.dll) Provides NTLM challenge/response authentication for client-server domains prior to Windows 2000 and for non-domain authentication (SMB/CIFS) - Digest Introduced in Windows XP (wdigest.dll) Provides challenge/response based HTTP and SASL authentication between Windows and non-Windows systems where Kerberos is not available - CredSSP Introduced in Windows Vista and available on Windows XP SP3 (credssp.dll) Provides SSO and Network Level Authentication for Remote Desktop Services - Schannel Introduced in Windows 2000 and updated in Windows Vista to support stronger AES encryption and ECC (schannel.dll) Microsoft's implementation of TLS/SSL Public key cryptography SSP that provides encryption and secure communication for authenticating clients and servers over the internet. Updated in Windows 7 to support TLS 1.2. If returns false, you can call GetLastError to get the reason for the failure } // Get the maximum authentication token size for this package ss := sspi.QuerySecurityPackageInfoA(PAnsiChar(packageName), packageInfo); if ss <> SEC_E_OK then begin RaiseWin32Error('QuerySecurityPackageInfo "'+PackageName+'" failed', ss); Result := ss; Exit; end; try cbMaxToken := packageInfo.cbMaxToken; finally FreeContextBuffer(packageInfo); end; // Initialize authorization identity structure ZeroMemory(@authIdentity, SizeOf(authIdentity)); if Length(domain) > 0 then begin authIdentity.Domain := PChar(Domain); authIdentity.DomainLength := Length(domain); end; if Length(userName) > 0 then begin authIdentity.User := PChar(UserName); authIdentity.UserLength := Length(UserName); end; if Length(Password) > 0 then begin authIdentity.Password := PChar(Password); authIdentity.PasswordLength := Length(Password); end; AuthIdentity.Flags := SEC_WINNT_AUTH_IDENTITY_ANSI; //SEC_WINNT_AUTH_IDENTITY_UNICODE ZeroMemory(@asClient, SizeOf(asClient)); ZeroMemory(@asserver, SizeOf(asserver)); //Allocate buffers for client and server messages GetMem(clientBuf, cbMaxToken); GetMem(serverBuf, cbMaxToken); try done := False; try // Prepare client message (negotiate) cbOut := cbMaxToken; ss := Self.GenClientContext(@asClient, authIdentity, packageName, nil, 0, clientBuf, cbOut, done); if ss < 0 then begin RaiseWin32Error('Error generating client context for negotiate', ss); Result := ss; Exit; end; // Prepare server message (challenge). cbIn := cbOut; cbOut := cbMaxToken; ss := Self.GenserverContext(@asserver, packageName, clientBuf, cbIn, serverBuf, cbOut, done); if ss < 0 then begin { Most likely failure: AcceptserverContext fails with SEC_E_LOGON_DENIED in the case of bad username or password. Unexpected Result: Logon will succeed if you pass in a bad username and the guest account is enabled in the specified domain. } RaiseWin32Error('Error generating server message for challenge', ss); Result := ss; Exit; end; // Prepare client message (authenticate). cbIn := cbOut; cbOut := cbMaxToken; ss := Self.GenClientContext(@asClient, authIdentity, packageName, serverBuf, cbIn, clientBuf, cbOut, done); if ss < 0 then begin RaiseWin32Error('Error generating client client for authenticate', ss); Result := ss; Exit; end; // Prepare server message (authentication). cbIn := cbOut; cbOut := cbMaxToken; ss := Self.GenserverContext(@asserver, packageName, clientBuf, cbIn, serverBuf, cbOut, done); if ss < 0 then begin RaiseWin32Error('Error generating server message for authentication', ss); Result := ss; Exit; end; finally //Free resources in client message if asClient.fHaveCtxtHandle then sspi.DeleteSecurityContext(@asClient.hctxt); if asClient.fHaveCredHandle then sspi.FreeCredentialHandle(@asClient.hcred); //Free resources in server message if asserver.fHaveCtxtHandle then sspi.DeleteSecurityContext(@asserver.hctxt); if asserver.fHaveCredHandle then sspi.FreeCredentialHandle(@asserver.hcred); end; finally FreeMem(clientBuf); FreeMem(serverBuf); end; Result := S_OK; end; 

注意 :任何代码发布到公共领域。 不需要归属

有一个叫做ldap_bind_s的win32 API函数。 ldap_bind_s函数根据LDAP验证客户端。 有关更多信息,请参阅MSDN文档。

我通过用户名和密码验证用户,如下所示:

用户名是Ldap服务器中的用户sn属性值,如U12345

userDN是Ldapserver中的用户DistinguishedName

 public bool AuthenticateUser(string username, string password) { try { var ldapserverNameAndPort = "servername:389"; var userDN = string.Format("CN=0},OU=Users,OU=MyOU,DC=MyDC,DC=com",username); var conn = new LdapConnection(ldapserverNameAndPort) { AuthType = AuthType.Basic }; conn.Bind(new NetworkCredential(userDN , password)); return true; } catch (Exception e) { return false; } 

}