65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 控制台通过屏蔽输入的字符来输入密码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (16投票s)

2004年8月28日

CPOL

8分钟阅读

viewsIcon

124324

downloadIcon

1182

演示如何拦截 .NET 控制台的键盘输入,并在屏幕上显示 * 来替代每个实际输入的字符。

Sample Image - ConsolePasswordInput.jpg

引言

我的一位同事曾问我,.NET 是否提供了任何控制台密码输入实用程序函数,以便应用程序能够接收和处理从键盘输入的字符,但不在屏幕上显示它们。取而代之的是,在输入的每个字符处输出一个 * 字符。

我不记得 .NET 中有任何此类实用程序,但我认为我可以用 C# 通过 DllImport 属性导入的 Windows 控制台 API 来开发一个。几天后,我成功创建了一个示例 .NET 控制台应用程序,其功能完全符合我同事的要求。

涉及技术总结

您需要使用 DllImportAttributeKernel32.DLL 导入几个控制台 API。

必须在 C# 中定义并使用与非托管 Kernel32 代码中使用的结构和联合匹配的结构和联合。StructLayoutAttribute 及其各种值(例如 LayoutKind)在这里很重要。FieldOffsetAttribute 对于在 C# 中模拟联合也很关键。

IntPtr struct 也很有用,它在 .NET 中定义,用于表示托管代码中的指针或句柄。

我还尝试了有效地使用委托来处理事件。

代码工作原理总结

我在 C# 代码中添加了大量注释,这些注释应作为文档。但是,代码的几个部分,包括我们拦截和重新显示键盘字符的顺序,需要更详细的解释。

这些将在下面总结

ConsolePasswordInput 类

键盘挂钩代码的主要逻辑封装在 ConsolePasswordInput 类中。此类使用通过 DllImport 导入的多个 Win32 控制台 API。

关键的控制台 API 是 ReadConsoleInput()。下面将对此进行更详细的解释。ConsolePasswordInput 类的大部分成员数据和函数都基于此 API 的功能逻辑。

接下来,我将深入探讨 ReadConsoleInput() API 及其相关的结构,然后再恢复讨论 ConsolePasswordInput 类。

ReadConsoleInput() API 和 INPUT_RECORD 结构

我们主要想拦截控制台键盘输入。我们通过使用 Win32 控制台 API ReadConsoleInput() 来实现这一目标。此方法会阻塞,直到发生控制台输入事件。控制台事件可以是键盘事件、鼠标事件、窗口缓冲区大小更改事件、菜单事件或控制台窗口焦点事件(有关更多详细信息,请参阅 MSDN 文档中的 ReadConsoleInput())。

在调用 ReadConsoleInput() 之前,我们必须定义一个 INPUT_RECORD 结构数组,该数组将在 ReadConsoleInput() 函数返回时被填充。

每个 INPUT_RECORD 结构都包含一个 KEY_EVENT_RECORDMOUSE_EVENTWINDOW_BUFFER_SIZE_EVENTMENU_EVENTFOCUS_EVENT 结构的联合,每个结构都与可能发生的事件类型相关。

INPUT_RECORD 结构及其联合在 C# 中定义如下

// The EventUnion struct is also treated as a union in the unmanaged world.
// We therefore use the StructLayoutAttribute and the FieldOffsetAttribute.
[StructLayout(LayoutKind.Explicit)]
internal struct EventUnion
{
 [FieldOffset(0)] internal KEY_EVENT_RECORD KeyEvent;
 [FieldOffset(0)] internal MOUSE_EVENT_RECORD MouseEvent;
 [FieldOffset(0)] internal WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
 [FieldOffset(0)] internal MENU_EVENT_RECORD MenuEvent;
 [FieldOffset(0)] internal FOCUS_EVENT_RECORD FocusEvent;
}
// The INPUT_RECORD structure is used within our application 
// to capture console input data.
internal struct INPUT_RECORD
{
 internal ushort EventType;
 internal EventUnion Event;
}

INPUT_RECORD 结构定义为一个普通的 C# 结构。Event 联合的类型为 EventUnion,它被定义为一个普通的 struct,但指定了 "StructLayoutAttribute" 和 "LayoutKind.Explicit" 值,以指示 EventUnion struct 的每个字段都用字节偏移量标记。

该字节偏移量由 "FieldOffsetAttribute" 指定,它表示内存中 struct 的起始位置与字段的起始位置之间的字节数。

正如您在 EventUnion struct 中看到的,字段 KeyEventMouseEventWindowBufferSizeEventMenuEventFocusEvent 被标记为偏移量为 0。这是非托管 C/C++ 联合在 C# 中表示的唯一方式。

代码查找哈希表和 ConsoleInputEvent 委托

现在我们继续讨论 ConsolePasswordInput 类。该类定义了一个哈希表 (htCodeLookup),用于将控制台事件类型映射到其事件处理程序。

我们定义了一个名为 ConsoleInputEvent 的委托,声明如下

// Declare a delegate to encapsulate a console event handler function.
// All event handler functions must return a boolean value indicating whether
// the password processing function should continue to read in another console
// input record (via ReadConsoleInput() API). 
// Returning a true indicates continue.
// Returning a false indicates don't continue.
internal delegate bool 
  ConsoleInputEvent(INPUT_RECORD input_record, ref string strBuildup);

如上面代码的注释所述,该委托封装了一个控制台事件处理函数。每个控制台事件处理函数必须接受一个 INPUT_RECORD 结构和一个字符串的引用,最后返回一个布尔值。

事件处理函数的作用是处理控制台事件(无论其类型如何),并在过程中构建密码字符串。它将根据密码字符串是否被认为已完全构建返回 true 或 false 值(稍后将详细介绍)。

代码查找哈希表在 ConsolePasswordInput() 类的构造过程中初始化

// Public constructor.
// Here, we prepare our hashtable of console input event handler functions.
public ConsolePasswordInput()
{
  htCodeLookup = new Hashtable();
  // Note well that we must cast Constant.* event numbers to ushort's.
  // This is because Constants.*_EVENT have been declared as of type int.
  // We could have, of course, declare Constants.*_EVENT to be of type ushort
  // but I deliberately declared them as ints to show the importance of 
  // types in C#.
  htCodeLookup.Add((object)((ushort)(Constants.KEY_EVENT)), 
                      new ConsoleInputEvent(KeyEventProc));
  htCodeLookup.Add((object)((ushort)(Constants.MOUSE_EVENT)), 
                      new ConsoleInputEvent(MouseEventProc));
  htCodeLookup.Add((object)((ushort)(Constants.WINDOW_BUFFER_SIZE_EVENT)), 
                      new ConsoleInputEvent(WindowBufferSizeEventProc));
  htCodeLookup.Add((object)((ushort)(Constants.MENU_EVENT)), 
                      new ConsoleInputEvent(MenuEventProc));
  htCodeLookup.Add((object)((ushort)(Constants.FOCUS_EVENT)), 
                      new ConsoleInputEvent(FocusEventProc));
}

请注意,我为所有五种类型的控制台事件都定义了处理程序,但只有 KeyEventProc() 函数不是微不足道的。这是因为我们只对在键盘输入过程中构建密码字符串感兴趣。当然,我们可以在其他事件期间执行密码处理(如果认为相关且有用)。

接下来,我们将检查 PasswordInput() 函数,并展示它如何使用各种事件处理程序来构建其密码字符串。

PasswordInput() 函数和 KeyEventProc() 函数

PasswordInput() 函数是 ConsolePasswordInput 类的主要公共函数。它首先初始化类的各种控制台句柄(hStdinhStdout),如果它们尚未初始化,然后暂时设置控制台模式以启用鼠标和窗口控制台输入事件。

此函数中的 while 循环是键盘字符挂钩的主要驱动力

// Main loop to collect characters typed into the console.
while (bContinueLoop == true)
{
  if
  (
    ReadConsoleInput
    (
      hStdin, // input buffer handle
      irInBuf, // buffer to read into
      128, // size of read buffer
      out cNumRead // number of records read
    ) == true
  )
  {
    // Dispatch the events to the appropriate handler.
    for (uint i = 0; i < cNumRead; i++)
    {
      // Lookup the hashtable for the appropriate
      // handler function... courtesy of Derek Kiong !
      ConsoleInputEvent cie_handler = 
        (ConsoleInputEvent)htCodeLookup[(object)(irInBuf[i].EventType)];
      // Note well that htCodeLookup may not have
      // the handler for the current event, 
      // so check first for a null value in cie_handler.
      if (cie_handler != null)
      {
        // Invoke the handler.
        bContinueLoop = cie_handler(irInBuf[i], ref refPasswordToBuild);
      }
    }
  }
}

这里,ReadConsoleInput() 函数在一个 while 循环中被调用。在每次循环中,都会调用 ReadConsoleInput() 函数。此函数会阻塞,直到发生控制台输入事件。

当发生此类控制台输入事件时,我们查找 htCodeLookup 哈希表,并确定与事件类型关联的 ConsoleInputEvent 委托。如果我们能找到一个委托,我们就调用它。

在我们的例子中,我们唯一感兴趣的委托是 KEY_EVENT 的委托。这就是为什么其他事件的委托会简单地返回 true,指示 while 循环继续循环。

让我们检查 KeyEventProc() 函数

// Event handler to handle a keyboard event. 
// We use this function to accumulate characters typed into the console and build
// up the password this way.
// All event handler functions must return a boolean value indicating whether
// the password processing function should continue to read in another console
// input record (via ReadConsoleInput() API). 
// Returning a true indicates continue.
// Returning a false indicates don't continue.
private bool KeyEventProc(INPUT_RECORD input_record, ref string strBuildup)
{
  // From the INPUT_RECORD, extract the KEY_EVENT_RECORD structure.
  KEY_EVENT_RECORD ker = input_record.Event.KeyEvent;
  // We process only during the keydown event.
  if (ker.bKeyDown != 0)
  {
    // This is to simulate a NULL handle value.
    IntPtr intptr = new IntPtr(0);
    // Get the current character pressed.
    char ch = (char)(ker.uchar.UnicodeChar);
    uint dwNumberOfCharsWritten = 0;
    // The character string that will be displayed
    // on the console screen.
    string strOutput = "*";
    // If we have received a Carriage Return character, we exit.
    if (ch == (char)'\r')
    {
      return false;
    }
    else
    {
      if (ch > 0)
      // The typed in key must represent a character
      // and must not be a control ley (e.g. SHIFT, ALT, CTRL, etc)
      {
        // A regular (non Carriage-Return character) is typed in...
        // We first display a '*' on the screen...
        WriteConsole
        (
          hStdout, // handle to screen buffer
          strOutput, // write buffer
          1, // number of characters to write
          ref dwNumberOfCharsWritten, // number of characters written
          intptr // reserved
        );
        // We build up our password string...
        string strConcat = new string(ch, 1);
        // by appending each typed in character at the end of strBuildup.
        strBuildup += strConcat;
        if (++iCounter < MaxNumberOfCharacters)
        {
          // Adding 1 to iCounter still makes iCounter
          // less than MaxNumberOfCharacters.
          // This means that the total number of characters
          // collected so far (this is 
          // equal to iCounter, by the way)
          // is less than MaxNumberOfCharacters.
          // We can carry on.
          return true;
        }
        else
        {
          // If, by adding 1 to iCounter makes iCounter
          // greater than MaxNumberOfCharacters,
          // it means that we have already collected
          // MaxNumberOfCharacters number of characters
          // inside strBuildup. We must exit now.
          return false;
        }
      }
    }
  }
  // The keydown state is false, we allow further characters to be typed in...
  return true;
}

由于 KeyEventProc() 函数被 PasswordInput() 函数的 while 循环反复调用,因此它会累积输入到控制台的字符,并以此方式构建密码。

在处理 KEY_EVENT 时,还有两个重要条件需要注意:是按下了键(而不是释放了键),以及是否只有一个控制键(SHIFT、CTRL 或 ALT)被按下。

我们通过以下 "if" 语句来处理第一个条件

 if (ker.bKeyDown != 0)...

位于函数开头附近。KEY_EVENT_RECORD.bKeyDown 字段指示此情况。请注意,ReadConsoleInput() 函数在按下键时返回,在释放键时也会返回(即使释放的是与最初按下的键相同的键)。这是 ReadConsoleInput() 函数的一个预期规范,并不令人惊讶。为了防止重复处理同一个键,我们只在按下键时执行操作,并忽略释放键的情况。

我们通过以下 "if" 语句来处理第二个条件

if (ch > 0) ...

就在 WriteConsole() 函数调用之前。这里 "ch" 是 char 类型,它包含 KEY_EVENT_RECORD.uchar.UnicodeChar 的副本。

请注意另一个重要点:ReadConsoleInput() 函数在按下控制键时返回。如果按下的是非控制键,KEY_EVENT_RECORD.uchar.UnicodeChar 中的值将是所按字符的 Unicode 号码。

非控制键当然可以与控制键一起按下。如果是这样,KEY_EVENT_RECORD.uchar.UnicodeChar 将包含一个有效的 Unicode 字符,并且 KEY_EVENT_RECORD.dwControlKeyState 字段将包含适当的值,指示控制键的适当状态(有关更多详细信息,请参阅 MSDN 文档中的 KEY_EVENT_RECORD struct)。

这一点很重要,因为它确保我们能够在密码中输入大写和小写字符。

但是,如果只按下一个控制键,KEY_EVENT_RECORD.uchar.UnicodeChar 将为零。这就是为什么我们检查 "ch" 是否非零。

由于键盘输入被 ReadConsoleInput() 函数拦截,因此默认情况下控制台不会显示输入的字符。我们的 KeyEventProc() 函数利用此机会通过 WriteConsole() 函数向控制台屏幕显示一个 '*' 字符。

当条件合适且密码被认为完全构建时(这将是按下回车键时,或者达到允许的最大密码字符数时),此函数将返回 false,以指示 PasswordInput() 函数停止其 while 循环。

Main() 函数

ConsolePasswordInput 类中包含一个 Main() 函数。此函数用作如何使用 ConsolePasswordInput 类的示例。

我们首先实例化此类的一个对象

ConsolePasswordInput cpi = new ConsolePasswordInput();

然后我们进入一个 while 循环,在其中反复调用 ConsolePasswordInput 对象的 PasswordInput() 函数来从用户那里获取密码

// Get an instance of the ConsolePasswordInput class to retrieve a password 
// of maximum size 5 characters.
while (iTries < 3)
{
  iTries++;
  strPassword = "";
  System.Console.Write ("Please enter your password : ");
  cpi.PasswordInput(ref strPassword, 20);
  System.Console.WriteLine();
  System.Console.WriteLine("Typed in password : {0}", strPassword);

  if (strPassword == "CodeProject")
  {
    System.Console.WriteLine ("Correct !");
    break;
  }
  else
  {
    if (iTries < 3)
    {
      System.Console.WriteLine ("try again...");
    }
    else
    {
      System.Console.WriteLine ("Wasted...");
    }
  }
}

我们这样做,直到用户尝试最多三次,或者密码正确(我们这里非常简单的示例使用 "CodeProject" 作为密码字符串)。

请注意,我们可以使用普通的 System.Console 行输出函数(Write()WriteLine())。我们 ConsolePasswordInput 类中的键盘挂钩函数不会干扰这些。

结论

本文的源代码包含完整的 Visual Studio .NET 解决方案项目文件。默认情况下它是一个应用程序,但可以轻松地更改为库。

除了展示如何拦截控制台键盘输入外,我还尝试展示如何在 C# 中构造联合。我还尝试展示委托和哈希表的有效使用。

我当然希望本文能对其他开发者有所帮助。

此致,

Bio。

© . All rights reserved.