windows Win32:如何根据 Active Directory 验证凭据?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/7111618/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Win32: How to validate credentials against Active Directory?
提问by Ian Boyd
It has been asked, and answered for .NET, but now it's time to get an answer for native Win32 code:
有人问过,并回答过 .NET,但现在是时候获得原生 Win32 代码的答案了:
How do i validate a Windows username and password?
如何验证 Windows 用户名和密码?
i asked this question before for managed code. Now it's time for the native solution.
我之前为托管代码问过这个问题。现在是本机解决方案的时候了。
It needs to be pointed the pitfalls with some of the more commonly proposed solutions:
需要指出一些更常见的解决方案的陷阱:
Invalid Method 1. Query Active Directory with Impersonation
无效方法 1. 使用模拟查询 Active Directory
A lot of people suggest querying the Active Directoryfor something. If an exception is thrown, then you know the credentials are not valid - as is suggested in this stackoverflow question.
很多人建议在 Active Directory 中查询某些内容。如果抛出异常,则您知道凭据无效 - 正如此 stackoverflow question 中所建议的那样。
There are some serious drawbacks to this approachhowever:
You are not only authenticating a domain account, but you are also doing an implicit authorization check. That is, you are reading properties from the AD using an impersonation token. What if the otherwise valid account has no rights to read from the AD? By default all users have read access, but domain policies can be set to disable access permissions for restricted accounts (and or groups).
Binding against the AD has a serious overhead, the AD schema cache has to be loaded at the client (ADSI cache in the ADSI provider used by DirectoryServices). This is both network, and AD server, resource consuming - and is too expensive for a simple operation like authenticating a user account.
You're relying on an exception failure for a non-exceptional case, and assuming that means invalid username and password. Other problems (e.g. network failure, AD connectivity failure, memory allocation error, etc) are then mis-intrepreted as authentication failure.
您不仅要对域帐户进行身份验证,还要进行隐式授权检查。也就是说,您正在使用模拟令牌从 AD 读取属性。如果原本有效的帐户没有读取 AD 的权限怎么办?默认情况下,所有用户都具有读取权限,但可以将域策略设置为禁用受限帐户(和/或组)的访问权限。
针对 AD 的绑定具有严重的开销,必须在客户端加载 AD 架构缓存(DirectoryServices 使用的 ADSI 提供程序中的 ADSI 缓存)。这既是网络又是 AD 服务器,会消耗资源 - 对于像验证用户帐户这样的简单操作来说成本太高。
您依赖于非异常情况的异常失败,并假设这意味着用户名和密码无效。其他问题(例如网络故障、AD 连接故障、内存分配错误等)则被误解为身份验证失败。
The use of the DirectoryEntry
class is .NET is an example of an incorrect way to verify credentials:
DirectoryEntry
.NET 类的使用是验证凭据的错误方法的一个示例:
Invalid Method 1a - .NET
无效的方法 1a - .NET
DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3");
object nativeObject = entry.NativeObject;
Invalid Method 1b - .NET #2
无效的方法 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;
}
}
}
}
As well as querying Active Directory through an ADO connection:
以及通过 ADO 连接查询 Active Directory:
Invalid Method 1c - Native Query
无效的方法 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'
These both fail even when your credentials are valid, but you do not have permission to view your directory entry:
即使您的凭据有效,这些都会失败,但您无权查看目录条目:
Invalid Method 2. LogonUser Win32 API
无效方法 2.LogonUser Win32 API
Othershave suggested using the LogonUser()API function. This sounds nice, but unfortunatly the calling user sometimes needs a permission ususally only given to the operating system itself:
其他人建议使用LogonUser()API 函数。这听起来不错,但不幸的是,调用用户有时需要通常只授予操作系统本身的权限:
The process calling LogonUser requires the SE_TCB_NAME privilege. If the calling process does not have this privilege, LogonUser fails and GetLastError returns ERROR_PRIVILEGE_NOT_HELD.
In some cases, the process that calls LogonUser must also have the SE_CHANGE_NOTIFY_NAME privilege enabled; otherwise, LogonUser fails and GetLastError returns ERROR_ACCESS_DENIED. This privilege is not required for the local system account or accounts that are members of the administrators group. By default, SE_CHANGE_NOTIFY_NAME is enabled for all users, but some administrators may disable it for everyone.
调用 LogonUser 的进程需要 SE_TCB_NAME 权限。如果调用进程没有此权限,则 LogonUser 失败并且 GetLastError 返回 ERROR_PRIVILEGE_NOT_HELD。
在某些情况下,调用 LogonUser 的进程还必须启用 SE_CHANGE_NOTIFY_NAME 权限;否则,LogonUser 失败并且 GetLastError 返回 ERROR_ACCESS_DENIED。本地系统帐户或作为管理员组成员的帐户不需要此权限。默认情况下,SE_CHANGE_NOTIFY_NAME 为所有用户启用,但某些管理员可能为每个人禁用它。
Handing out the "Act as a part of the operating system" privelage is not something you want to do willy-nilly - as Microsoft points out in a knowledge base article:
分发“作为操作系统的一部分”特权不是您愿意做的事情 - 正如微软在知识库文章中指出的那样:
...the process that is calling LogonUser must have the SE_TCB_NAME privilege (in User Manager, this is the "Act as part of the Operating System" right). The SE_TCB_NAME privilege is very powerful and should not be granted to any arbitrary user just so that they can run an applicationthat needs to validate credentials.
...调用 LogonUser 的进程必须具有 SE_TCB_NAME 权限(在用户管理器中,这是“作为操作系统的一部分”权限)。SE_TCB_NAME 权限非常强大, 不应授予任何任意用户,以便他们可以运行需要验证凭据的应用程序。
Additionally, a call to LogonUser() will fail if a blank password is specified.
此外,如果指定了空白密码,则对 LogonUser() 的调用将失败。
Valid .NET 3.5 Method - PrincipalContext
有效的 .NET 3.5 方法 - PrincipalContext
There is a validation method, only available in .NET 3.5 and newer, that allows authentication by a user without performing an authorization check:
有一种验证方法,仅在 .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")
}
Unfortunately this code is only available in .NET 3.5 and later.
不幸的是,此代码仅在 .NET 3.5 及更高版本中可用。
It's time to find the nativeequivalent.
是时候找到本地的等价物了。
采纳答案by Luke
Here is Microsoft's recommendation.
As for the other answers, I'm not really sure why you're shooting them down. You are complaining about (relatively edge case) failures while trying to validate credentials, but if you are going to actually do something with those credentials then that operation is just going to fail anyway. If you are not going to actually do something with those credentials, then why do you need to validate them in the first place? It seems like a somewhat contrived situation, but obviously I don't know what you're trying to accomplish.
至于其他答案,我不太确定你为什么要击落它们。您在尝试验证凭据时抱怨(相对边缘情况)失败,但是如果您要实际使用这些凭据执行某些操作,那么无论如何该操作都会失败。如果您不打算实际使用这些凭据做某事,那么为什么首先需要验证它们?这似乎有点人为的情况,但显然我不知道你想要完成什么。
回答by John
For the native equivalnt of your valid .NET solution see thisMSDN page and ldap_bind
有关有效 .NET 解决方案的本机等效项,请参阅此MSDN 页面和ldap_bind
Howerver I think that LogonUser
is the right API for the task when use with LOGON32_LOGON_NETWORK
. Note that the limitation of SE_CHANGE_NOTIFY_NAME
is only for Windows 2000 (so Windows XP and newer do not require this priviledge) and that by default SE_CHANGE_NOTIFY_NAME is enabled for all users. Also the MSDN page says
但是,我认为LogonUser
当与LOGON32_LOGON_NETWORK
. 请注意,限制SE_CHANGE_NOTIFY_NAME
仅适用于 Windows 2000(因此 Windows XP 和更新版本不需要此权限)并且默认情况下为所有用户启用 SE_CHANGE_NOTIFY_NAME。MSDN页面也说
The SE_TCB_NAME privilege is not required for this function unless you are logging onto a Passport account.
除非您登录 Passport 帐户,否则此功能不需要 SE_TCB_NAME 权限。
In this case you are logging onto an AD account so SE_TCB_NAME is not required.
在这种情况下,您登录的是 AD 帐户,因此不需要 SE_TCB_NAME。
回答by Ian Boyd
I might as well post the native code to validate a set of Windows credentials. It took a while to implement.
我不妨发布本机代码来验证一组 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;
Note: Any code released into public domain. No attribution required.
注意:任何发布到公共领域的代码。不需要归属。
回答by Hans
回答by nahidf
I authenticated user, by username & password like this :
我通过用户名和密码对用户进行身份验证,如下所示:
username is user sn attribute value in Ldap server, like U12345
username 是 LDAP 服务器中的用户 sn 属性值,如 U12345
userDN is user DistinguishedName in LdapServer
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;
}
}
}