简体   繁体   English

Windows 安全自定义登录验证

[英]Windows Security Custom login validation

I'm creating an Xaml/C# application and I would like it to popup with a Login Prompt.我正在创建一个 Xaml/C# 应用程序,我希望它弹出一个登录提示。

I would like to know if its possible to use CredUIPromptForWindowsCredentials.我想知道是否可以使用 CredUIPromptForWindowsCredentials。

  • Show Windows Security Dialog显示 Windows 安全对话框
  • Get the entered username & password获取输入的用户名和密码
  • Perform Custom validation执行自定义验证
  • If validation succes -> continue app如果验证成功 -> 继续应用
  • else if validation failed -> -inform user of invalid username or password否则如果验证失败 -> - 通知用户无效的用户名或密码

I have already looked at Windows Security login form?我已经看过Windows 安全登录表单? and http://www.pinvoke.net/default.aspx/credui/creduipromptforwindowscredentials.html?diff=y but they don't explain how to handle the validation.http://www.pinvoke.net/default.aspx/credui/creduipromptforwindowscredentials.html?diff=y但他们没有解释如何处理验证。

I would really like a small example, where if the user enters username = "Bo" and password = "123" then succes else display error message and allow the user to try again.我真的很喜欢一个小例子,如果用户输入 username = "Bo" 和 password = "123" 然后成功显示错误消息并允许用户重试。

The App is going to be installed on multiple computers.该应用程序将安装在多台计算机上。

Or is this simply not possible?或者这根本不可能?

Update更新

Inspired by the answer in this question Show Authentication dialog in C# for windows Vista/7灵感来自这个问题的答案Show Authentication dialog in C# for windows Vista/7

I have modified the code to work as expected.我已经修改了代码以按预期工作。

Please not, that the validation part is only for proof of concept.请注意,验证部分仅用于概念验证。

WindowsSecurityDialog.cs WindowsSecurityDialog.cs

 public class WindowsSecurityDialog
    {

       public string CaptionText { get; set; }
       public string MessageText { get; set; }

        [DllImport("ole32.dll")]
        public static extern void CoTaskMemFree(IntPtr ptr);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private struct CREDUI_INFO
        {
            public int cbSize;
            public IntPtr hwndParent;
            public string pszMessageText;
            public string pszCaptionText;
            public IntPtr hbmBanner;
        }


        [DllImport("credui.dll", CharSet = CharSet.Auto)]
        private static extern bool CredUnPackAuthenticationBuffer(int dwFlags,
                                                                   IntPtr pAuthBuffer,
                                                                   uint cbAuthBuffer,
                                                                   StringBuilder pszUserName,
                                                                   ref int pcchMaxUserName,
                                                                   StringBuilder pszDomainName,
                                                                   ref int pcchMaxDomainame,
                                                                   StringBuilder pszPassword,
                                                                   ref int pcchMaxPassword);

        [DllImport("credui.dll", CharSet = CharSet.Auto)]
        private static extern int CredUIPromptForWindowsCredentials(ref CREDUI_INFO notUsedHere,
                                                                     int authError,
                                                                     ref uint authPackage,
                                                                     IntPtr InAuthBuffer,
                                                                     uint InAuthBufferSize,
                                                                     out IntPtr refOutAuthBuffer,
                                                                     out uint refOutAuthBufferSize,
                                                                     ref bool fSave,
                                                                     int flags);



        public bool ValidateUser()
        {
            var credui = new CREDUI_INFO
                                     {
                                         pszCaptionText = CaptionText,
                                         pszMessageText = MessageText
                                     };
            credui.cbSize = Marshal.SizeOf(credui);
            uint authPackage = 0;
            IntPtr outCredBuffer;
            uint outCredSize;
            bool save = false;


            const int loginErrorCode = 1326;    //Login Failed
            var authError = 0;

            while (true)
            {




                var result = CredUIPromptForWindowsCredentials(ref credui,
                                                               authError,
                                                               ref authPackage,
                                                               IntPtr.Zero,
                                                               0,
                                                               out outCredBuffer,
                                                               out outCredSize,
                                                               ref save,
                                                               1 /* Generic */);

                var usernameBuf = new StringBuilder(100);
                var passwordBuf = new StringBuilder(100);
                var domainBuf = new StringBuilder(100);

                var maxUserName = 100;
                var maxDomain = 100;
                var maxPassword = 100;
                if (result == 0)
                {
                    if (CredUnPackAuthenticationBuffer(0, outCredBuffer, outCredSize, usernameBuf, ref maxUserName,
                                                       domainBuf, ref maxDomain, passwordBuf, ref maxPassword))
                    {
                        //TODO: ms documentation says we should call this but i can't get it to work
                        //SecureZeroMem(outCredBuffer, outCredSize);

                        //clear the memory allocated by CredUIPromptForWindowsCredentials 
                        CoTaskMemFree(outCredBuffer);
                        var networkCredential = new NetworkCredential()
                                                {
                                                    UserName = usernameBuf.ToString(),
                                                    Password = passwordBuf.ToString(),
                                                    Domain = domainBuf.ToString()
                                                };

                        //Dummy Code replace with true User Validation
                        if (networkCredential.UserName == "Bo" && networkCredential.Password == "1234")
                            return true;
                        else //login failed show dialog again with login error
                        {
                            authError = loginErrorCode;
                        }



                    }
                }
                else return false;


            }
        }
    }

App.xaml.cs应用程序.xaml.cs

protected override void OnStartup(StartupEventArgs e)
        {
            var windowsSecurityDialog = new WindowsSecurityDialog
                                            {
                                                CaptionText = "Enter your credentials",
                                                MessageText = "These credentials will be used to connect to YOUR APP NAME";
                                            };

            if (windowsSecurityDialog.ValidateUser())
                base.OnStartup(e);
        }

您将在Ookii dialogs 中找到使用 CredUIPromptForWindowsCredentials 的 WPF 和 WinForms 的完整实现。

I was a little horrified when I started thinking this might be possible.当我开始认为这可能是可能的时,我有点害怕。

The answer is yes and no.答案是肯定的和否定的。 You can get a hold of the network domain and username, but (thank goodness), you can't get a hold of the actual password, only a hash of the password.您可以获取网络域和用户名,但是(谢天谢地),您无法获取实际密码,只能获取密码的哈希值。

Borrowing heavily from PInvoke , here's a sample WPF App that brings in and outputs the username and password.PInvoke大量借用,这里有一个示例 WPF 应用程序,它引入和输出用户名和密码。

Code代码

using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Interop;

namespace LoginDialog
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // Declare/initialize variables.
            bool save = false;
            int errorcode = 0;
            uint dialogReturn;
            uint authPackage = 0;
            IntPtr outCredBuffer;
            uint outCredSize;

            // Create the CREDUI_INFO struct.
            CREDUI_INFO credui = new CREDUI_INFO();
            credui.cbSize = Marshal.SizeOf(credui);
            credui.pszCaptionText = "Connect to your application";
            credui.pszMessageText = "Enter your credentials!";
            credui.hwndParent = new WindowInteropHelper(this).Handle;

            // Show the dialog.
            dialogReturn = CredUIPromptForWindowsCredentials(
                ref credui, 
                errorcode, 
                ref authPackage,
                (IntPtr)0,  // You can force that a specific username is shown in the dialog. Create it with 'CredPackAuthenticationBuffer()'. Then, the buffer goes here...
                0,          // ...and the size goes here. You also have to add CREDUIWIN_IN_CRED_ONLY to the flags (last argument).
                out outCredBuffer, 
                out outCredSize, 
                ref save, 
                0); // Use the PromptForWindowsCredentialsFlags Enum here. You can use multiple flags if you seperate them with | .

            if (dialogReturn == 1223) // Result of 1223 means the user canceled. Not sure if other errors are ever returned.
                textBox1.Text += ("User cancelled!");
            if (dialogReturn != 0) // Result of something other than 0 means...something, I'm sure. Either way, failed or canceled.
                return;

            var domain = new StringBuilder(100);
            var username = new StringBuilder(100);
            var password = new StringBuilder(100);
            int maxLength = 100; // Note that you can have different max lengths for each variable if you want.

            // Unpack the info from the buffer.
            CredUnPackAuthenticationBuffer(0, outCredBuffer, outCredSize, username, ref maxLength, domain, ref maxLength, password, ref maxLength);

            // Clear the memory allocated by CredUIPromptForWindowsCredentials.
            CoTaskMemFree(outCredBuffer);

            // Output info, escaping whitespace characters for the password.
            textBox1.Text += String.Format("Domain: {0}\n", domain);
            textBox1.Text += String.Format("Username: {0}\n", username);
            textBox1.Text += String.Format("Password (hashed): {0}\n", EscapeString(password.ToString()));
        }

        public static string EscapeString(string s)
        {
            // Formatted like this only for you, SO.
            return s
                .Replace("\a", "\\a")
                .Replace("\b", "\\b")
                .Replace("\f", "\\f")
                .Replace("\n", "\\n")
                .Replace("\r", "\\r")
                .Replace("\t", "\\t")
                .Replace("\v", "\\v");
        }

        #region DLLImports
        [DllImport("ole32.dll")]
        public static extern void CoTaskMemFree(IntPtr ptr);

        [DllImport("credui.dll", CharSet = CharSet.Unicode)]
        private static extern uint CredUIPromptForWindowsCredentials(ref CREDUI_INFO notUsedHere, int authError, ref uint authPackage, IntPtr InAuthBuffer,
          uint InAuthBufferSize, out IntPtr refOutAuthBuffer, out uint refOutAuthBufferSize, ref bool fSave, PromptForWindowsCredentialsFlags flags);

        [DllImport("credui.dll", CharSet = CharSet.Unicode)]
        private static extern bool CredUnPackAuthenticationBuffer(int dwFlags, IntPtr pAuthBuffer, uint cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword);
        #endregion

        #region Structs and Enums
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct CREDUI_INFO
        {
            public int cbSize;
            public IntPtr hwndParent;
            public string pszMessageText;
            public string pszCaptionText;
            public IntPtr hbmBanner;
        }

        private enum PromptForWindowsCredentialsFlags
        {
            /// <summary>
            /// The caller is requesting that the credential provider return the user name and password in plain text.
            /// This value cannot be combined with SECURE_PROMPT.
            /// </summary>
            CREDUIWIN_GENERIC = 0x1,
            /// <summary>
            /// The Save check box is displayed in the dialog box.
            /// </summary>
            CREDUIWIN_CHECKBOX = 0x2,
            /// <summary>
            /// Only credential providers that support the authentication package specified by the authPackage parameter should be enumerated.
            /// This value cannot be combined with CREDUIWIN_IN_CRED_ONLY.
            /// </summary>
            CREDUIWIN_AUTHPACKAGE_ONLY = 0x10,
            /// <summary>
            /// Only the credentials specified by the InAuthBuffer parameter for the authentication package specified by the authPackage parameter should be enumerated.
            /// If this flag is set, and the InAuthBuffer parameter is NULL, the function fails.
            /// This value cannot be combined with CREDUIWIN_AUTHPACKAGE_ONLY.
            /// </summary>
            CREDUIWIN_IN_CRED_ONLY = 0x20,
            /// <summary>
            /// Credential providers should enumerate only administrators. This value is intended for User Account Control (UAC) purposes only. We recommend that external callers not set this flag.
            /// </summary>
            CREDUIWIN_ENUMERATE_ADMINS = 0x100,
            /// <summary>
            /// Only the incoming credentials for the authentication package specified by the authPackage parameter should be enumerated.
            /// </summary>
            CREDUIWIN_ENUMERATE_CURRENT_USER = 0x200,
            /// <summary>
            /// The credential dialog box should be displayed on the secure desktop. This value cannot be combined with CREDUIWIN_GENERIC.
            /// Windows Vista: This value is not supported until Windows Vista with SP1.
            /// </summary>
            CREDUIWIN_SECURE_PROMPT = 0x1000,
            /// <summary>
            /// The credential provider should align the credential BLOB pointed to by the refOutAuthBuffer parameter to a 32-bit boundary, even if the provider is running on a 64-bit system.
            /// </summary>
            CREDUIWIN_PACK_32_WOW = 0x10000000,
        }
        #endregion
    }
}

Test测试

  1. Create a new WPF application called LoginDialog .创建一个名为LoginDialog的新 WPF 应用程序。
  2. Drop a TextBox into the MainWindow.xaml file provided named textBox1 .将一个TextBox放入提供的名为textBox1MainWindow.xaml文件中。
  3. Replace the code in the MainWindow.xaml.cs file.替换MainWindow.xaml.cs文件中的代码。
  4. Run!跑步!

Sample Output样本输出

Given the password "password", here is the output.给定密码“password”,这里是输出。

Domain: 
Username: EXAMPLE\fake
Password (hashed): @@D\a\b\f\n\rgAAAAAU-JPAAAAAAweFpM4nPlOUfKi83JLsl4jjh6nMX34yiH

Comments评论

This works for WPF.这适用于 WPF。 It can work for Silverlight with the right permissions .它可以在具有正确权限的Silverlight 中工作。

I don't know why anyone would ever do this for legitimate custom validation.我不知道为什么有人会为了合法的自定义验证而这样做。 If you want to create a login for your app, I'd suggest having the client connect via SSL (https://) to an ASP.NET page or web service that will check the credentials provided using LINQ to SQL.如果您想为您的应用程序创建登录名,我建议让客户端通过 SSL (https://) 连接到 ASP.NET 页面或 Web 服务,该页面或 Web 服务将检查使用 LINQ to SQL 提供的凭据。 It can then send the client a pass/fail response.然后它可以向客户端发送通过/失败响应。

Oh, and for the love of god and all that is holy, salt and hash your users' passwords .哦,为了上帝的爱和所有神圣的东西,盐和散列用户的密码

Note: If you're wanting to use this login to prevent the user from using your app without having an account/paying, all the above stands, but will not be sufficient to stop people from reverse engineering and cracking the app (eg, tricking it into thinking that it's received the pass message).注意:如果您想使用此登录名来防止用户在没有帐户/付款的情况下使用您的应用程序,上述所有内容均有效,但不足以阻止人们逆向工程和破解应用程序(例如,欺骗它认为它已收到传递消息)。 That sort of DRM is a whole 'nother ballgame.那种 DRM 完全是“另一种球赛”。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM