【问题标题】:How to validate domain credentials?如何验证域凭据?
【发布时间】:2010-09-24 12:26:20
【问题描述】:

我想针对域控制器验证一组凭据。例如:

Username: STACKOVERFLOW\joel
Password: splotchy

方法一、模拟查询Active Directory

​​>

很多人建议在 Active Directory 中查询某些内容。如果抛出异常,那么您就知道凭据无效 - 正如 this stackoverflow question 中所建议的那样。

不过有一些严重的drawbacks to this approach

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

  2. 针对 AD 的绑定具有严重的开销,必须在客户端加载 AD 架构缓存(ADSI 缓存在 DirectoryServices 使用的 ADSI 提供程序中)。这既是网络,又是 AD 服务器,会消耗资源 - 并且对于验证用户帐户等简单操作而言过于昂贵。

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

方法2.LogonUser Win32 API

Others 建议使用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 是 为所有用户启用,但有些 管理员可以禁用它 大家。

分发“作为操作系统的一部分”特权并不是你想做的事——正如微软在knowledge base article 中指出的那样:

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

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


验证一组域凭据的正确方法是什么?


碰巧从托管代码调用,但这是一个一般的 Windows 问题。可以假设客户已经安装了 .NET Framework 2.0。

【问题讨论】:

  • 读者应该注意,从 Windows XP 开始,LogonUser 不再需要 SE_TCB_NAME(除非您正在登录 Passport 帐户)。

标签: c# windows security authentication


【解决方案1】:

.NET 3.5 中的 C# 使用 System.DirectoryServices.AccountManagement

 bool valid = false;
 using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
 {
     valid = context.ValidateCredentials( username, password );
 }

这将针对当前域进行验证。查看参数化的 PrincipalContext 构造函数以获取其他选项。

【讨论】:

  • @tvanfosson:DirectoryServices 不使用 AD 吗?
  • 是的。但是文档表明这是一种验证凭据的快速方法。它也不同于问题中提到的绑定方法,因为您没有从对象中读取任何属性。请注意,该方法是在上下文中,而不是目录对象。
  • 更正:System.DirectoryServices.AccountManagement 需要 .NET 3.5。 (msdn.microsoft.com/en-us/library/…)
  • 如果您改用new PrincipalContext(ContextType.Machine),它也适用于本地用户。
  • 有谁知道这是否适用于缓存的凭据,还是需要连接到 DC?我需要知道这一点,以了解我现在正在进行的一些实现,并且我目前没有在任何域上进行测试
【解决方案2】:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.DirectoryServices.AccountManagement;

public struct Credentials
{
    public string Username;
    public string Password;
}

public class Domain_Authentication
{
    public Credentials Credentials;
    public string Domain;

    public Domain_Authentication(string Username, string Password, string SDomain)
    {
        Credentials.Username = Username;
        Credentials.Password = Password;
        Domain = SDomain;
    }

    public bool IsValid()
    {
        using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Domain))
        {
            // validate the credentials
            return pc.ValidateCredentials(Credentials.Username, Credentials.Password);
        }
    }
}

【讨论】:

  • 这与@tvanfosson 3 年前的回答是否有任何显着差异?
  • @gbjbaanb 是的,因为它在创建PrincipalContext 时包含Domain 参数,这是我有兴趣知道并在此答案中找到的。
  • @RudiVisser tvanfosson 确实建议您“查看参数化的 PrincipalContext 构造函数以获取其他选项” - 始终阅读文档,永远不要只听互联网的话! :)
  • @gbjbaanb 当然是的,但是提供一个工作示例而不是链接和建议在其他地方阅读是 StackOverflow 的口头禅,这就是为什么我们接受多个答案提交的原因:D 简单地说这个 确实提供更多。
  • 有谁知道我们如何在 UWP 应用程序中做类似的事情? (使用常规 AD 而不是 Azure AD)。我在这里问了一个问题:stackoverflow.com/questions/42821447
【解决方案3】:

我正在使用以下代码来验证凭据。 下面显示的方法将确认凭据是否正确,如果不正确,则密码是否已过期或需要更改。

我一直在寻找类似的东西......所以我希望这对某人有帮助!

using System;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Runtime.InteropServices;

namespace User
{
    public static class UserValidation
    {
        [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);
        enum LogonProviders : uint
        {
            Default = 0, // default for platform (use this!)
            WinNT35,     // sends smoke signals to authority
            WinNT40,     // uses NTLM
            WinNT50      // negotiates Kerb or NTLM
        }
        enum LogonTypes : uint
        {
            Interactive = 2,
            Network = 3,
            Batch = 4,
            Service = 5,
            Unlock = 7,
            NetworkCleartext = 8,
            NewCredentials = 9
        }
        public  const int ERROR_PASSWORD_MUST_CHANGE = 1907;
        public  const int ERROR_LOGON_FAILURE = 1326;
        public  const int ERROR_ACCOUNT_RESTRICTION = 1327;
        public  const int ERROR_ACCOUNT_DISABLED = 1331;
        public  const int ERROR_INVALID_LOGON_HOURS = 1328;
        public  const int ERROR_NO_LOGON_SERVERS = 1311;
        public  const int ERROR_INVALID_WORKSTATION = 1329;
        public  const int ERROR_ACCOUNT_LOCKED_OUT = 1909;      //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
        public  const int ERROR_ACCOUNT_EXPIRED = 1793;
        public  const int ERROR_PASSWORD_EXPIRED = 1330;

        public static int CheckUserLogon(string username, string password, string domain_fqdn)
        {
            int errorCode = 0;
            using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain_fqdn, "ADMIN_USER", "PASSWORD"))
            {
                if (!pc.ValidateCredentials(username, password))
                {
                    IntPtr token = new IntPtr();
                    try
                    {
                        if (!LogonUser(username, domain_fqdn, password, LogonTypes.Network, LogonProviders.Default, out token))
                        {
                            errorCode = Marshal.GetLastWin32Error();
                        }
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                    finally
                    {
                        CloseHandle(token);
                    }
                }
            }
            return errorCode;
        }
    }

【讨论】:

  • 这是问题中描述的“方法2”......所以......没有真正回答问题
【解决方案4】:

确定本地用户的方法如下:

    public bool IsLocalUser()
    {
        return windowsIdentity.AuthenticationType == "NTLM";
    }

伊恩·博伊德编辑

您根本不应该再使用 NTLM。它太老了,太糟糕了,以至于微软的应用程序验证器(用于捕获常见的编程错误)如果检测到你使用 NTLM 就会发出警告。

以下是应用程序验证器文档中的一章,说明了如果有人错误地使用 NTLM,他们为什么要进行测试:

为什么需要 NTLM 插件

NTLM 是一种过时的身份验证协议,存在以下缺陷 可能会危及应用程序的安全性和操作 系统。最大的缺点是缺少服务器 身份验证,这可能允许攻击者诱骗用户进入 连接到欺骗服务器。作为缺少服务器的必然结果 身份验证,使用 NTLM 的应用程序也可能容易受到 一种称为“反射”攻击的攻击。这后者允许一个 攻击者将用户的身份验证对话劫持到 合法服务器并使用它来向攻击者验证 用户的计算机。 NTLM 的漏洞和利用它们的方法 是增加安全研究活动的目标 社区。​​p>

尽管 Kerberos 已经存在很多年了,但许多应用程序 仍然编写为仅使用 NTLM。这不必要地减少了 应用程序的安全性。然而,Kerberos 不能完全取代 NTLM 场景——主要是客户端需要验证的场景 未加入域的系统(可能是家庭网络 其中最常见的)。协商安全包允许 尽可能使用 Kerberos 的向后兼容妥协 并且只有在没有其他选项时才恢复为 NTLM。切换代码 使用 Negotiate 而不是 NTLM 将显着增加 为我们的客户提供安全性,同时引入很少或不引入应用程序 兼容性。谈判本身并不是灵丹妙药——那里 是攻击者可以强制降级到 NTLM 的情况,但这些是 明显更难利用。然而,一个立即 改进之处在于编写的应用程序可以正确使用 Negotiate 自动免疫 NTLM 反射攻击。

最后告诫不要使用 NTLM:将来 Windows 版本可以在以下位置禁用 NTLM 操作系统。如果应用程序对 NTLM 有硬依赖 当 NTLM 被禁用时,他们将无法进行身份验证。

插件的工作原理

验证器插件检测到以下错误:

  • 在对 AcquireCredentialsHandle(或更高级别的包装 API)的调用中直接指定 NTLM 包。

  • InitializeSecurityContext 调用中的目标名称为 NULL。

  • InitializeSecurityContext 调用中的目标名称不是格式正确的 SPN、UPN 或 NetBIOS 样式的域名。

后两种情况将强制 Negotiate 直接(第一种情况)或间接回退到 NTLM(在第二种情况下,域控制器将返回“principal not found”错误,导致 Negotiate 回退)。

该插件还会在检测到降级到 NTLM 时记录警告;例如,当域控制器找不到 SPN 时。这些仅记录为警告,因为它们通常是合法的情况 - 例如,在对未加入域的系统进行身份验证时。

NTLM 停止

5000 - 应用程序已明确选择 NTLM 包

严重性 - 错误

应用程序或子系统在对 AcquireCredentialsHandle 的调用中明确选择 NTLM 而不是 Negotiate。即使客户端和服务器可以使用 Kerberos 进行身份验证,这也可以通过显式选择 NTLM 来阻止。

如何修复此错误

解决此错误的方法是选择协商包代替 NTLM。如何完成将取决于客户端或服务器使用的特定网络子系统。下面给出了一些例子。您应该查阅有关您正在使用的特定库或 API 集的文档。

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”

【讨论】:

    【解决方案5】:
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.DirectoryServices.AccountManagement;
    
    class WindowsCred
    {
        private const string SPLIT_1 = "\\";
    
        public static bool ValidateW(string UserName, string Password)
        {
            bool valid = false;
            string Domain = "";
    
            if (UserName.IndexOf("\\") != -1)
            {
                string[] arrT = UserName.Split(SPLIT_1[0]);
                Domain = arrT[0];
                UserName = arrT[1];
            }
    
            if (Domain.Length == 0)
            {
                Domain = System.Environment.MachineName;
            }
    
            using (PrincipalContext context = new PrincipalContext(ContextType.Domain, Domain)) 
            {
                valid = context.ValidateCredentials(UserName, Password);
            }
    
            return valid;
        }
    }
    

    卡希夫·穆斯塔克 加拿大渥太华

    【讨论】:

    • System.DirectoryServices.AccountManagement 命名空间是 .NET 3.5 中的新名称
    • 我知道这已经快 4 年了,但是如果您要验证本地用户,则需要确保在构造 PrincipalContext 时将 ContextType 设置为 ContextType.Machine。否则它会认为 Domain 变量中提供的机器名实际上是一个域服务器。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-10
    • 1970-01-01
    • 2012-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-26
    相关资源
    最近更新 更多