WPF中的自定义掩码密码框

5

在iOS中,当您在字段中输入密码时,字段的最后一个字母会显示,但在您输入下一个字符时会被模糊处理。有没有办法在WPF中复制这种行为?


3
如果你在桌面应用程序上这样做,那么这被认为是不好的做法。要考虑到在手机上,别人从肩膀后面看屏幕的可能性大大降低,而在桌面上则不同。所以除非你打算在Windows手机或平板电脑上使用此功能,否则请重新考虑。 - Viv
2
@Viv的想法很合理,但不幸的是,在这种情况下我并不是在指定需求。 - aceinthehole
@Viv Plus,这个应用程序可能会被视力受损的人使用,这种情况下可能更有意义。 - aceinthehole
可以使用附加属性(http://wpftutorial.net/PasswordBox.html)来完成,但我怀疑一位优秀的IT安全官员是否会允许其部署。 - Gayot Fow
1个回答

15

如果您需要在桌面应用中使用此类功能,可以采取以下方法:

我们之前也有过类似的需求,我采取了如下措施:

  • 我通过从TextBox派生并添加一个新的类型为SecureString的DP(与普通的PasswordBox相同)来创建一个自定义的Passwordbox。这样做不会失去任何安全性,并且可以自定义视觉行为。
  • 现在,我们可以将TextBoxText用作其显示字符串,并将实际密码保存在后端的SecureString DP中并将其绑定到VM。
  • 我们处理PreviewTextInputPreviewKeyDown事件来管理控件中的所有文本更改,包括一些非常烦人的键盘按键(如Key.Back, Key.Delete和不经过PreviewTextInputKey.Space

iOS感受:

还有几件事情需要注意以获得精确的iOS行为:

  1. 仅在向“当前字符串的末尾”添加新字符时显示最后一个字符(与FlowDirection无关)
  2. 在现有字符串中间编辑字符不会影响掩码。
  3. 显示的最后一个字符是计时器相关(如果闲置一段时间,会变成“*”)
  4. 在控件中禁用所有复制粘贴操作。

当检测到文本更改时,前两点很容易处理,对于最后一点,我们可以使用DispatcherTimer相应地处理显示字符串。

因此,将所有这些组合在一起,我们得到:

/// <summary>
///   This class contains properties for CustomPasswordBox
/// </summary>
internal class CustomPasswordBox : TextBox {
  #region Member Variables
  /// <summary>
  ///   Dependency property to hold watermark for CustomPasswordBox
  /// </summary>
  public static readonly DependencyProperty PasswordProperty =
    DependencyProperty.Register(
      "Password", typeof(SecureString), typeof(CustomPasswordBox), new UIPropertyMetadata(new SecureString()));

  /// <summary>
  ///   Private member holding mask visibile timer
  /// </summary>
  private readonly DispatcherTimer _maskTimer;
  #endregion

  #region Constructors
  /// <summary>
  ///   Initialises a new instance of the LifeStuffPasswordBox class.
  /// </summary>
  public CustomPasswordBox() {
    PreviewTextInput += OnPreviewTextInput;
    PreviewKeyDown += OnPreviewKeyDown;
    CommandManager.AddPreviewExecutedHandler(this, PreviewExecutedHandler);
    _maskTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1) };
    _maskTimer.Tick += (sender, args) => MaskAllDisplayText();
  }
  #endregion

  #region Commands & Properties
  /// <summary>
  ///   Gets or sets dependency Property implementation for Password
  /// </summary>
  public SecureString Password {
    get {
      return (SecureString)GetValue(PasswordProperty);
    }

    set {
      SetValue(PasswordProperty, value);
    }
  }
  #endregion

  #region Methods
  /// <summary>
  ///   Method to handle PreviewExecutedHandler events
  /// </summary>
  /// <param name="sender">Sender object</param>
  /// <param name="executedRoutedEventArgs">Event Text Arguments</param>
  private static void PreviewExecutedHandler(object sender, ExecutedRoutedEventArgs executedRoutedEventArgs) {
    if (executedRoutedEventArgs.Command == ApplicationCommands.Copy ||
        executedRoutedEventArgs.Command == ApplicationCommands.Cut ||
        executedRoutedEventArgs.Command == ApplicationCommands.Paste) {
      executedRoutedEventArgs.Handled = true;
    }
  }

  /// <summary>
  ///   Method to handle PreviewTextInput events
  /// </summary>
  /// <param name="sender">Sender object</param>
  /// <param name="textCompositionEventArgs">Event Text Arguments</param>
  private void OnPreviewTextInput(object sender, TextCompositionEventArgs textCompositionEventArgs) {
    AddToSecureString(textCompositionEventArgs.Text);
    textCompositionEventArgs.Handled = true;
  }

  /// <summary>
  ///   Method to handle PreviewKeyDown events
  /// </summary>
  /// <param name="sender">Sender object</param>
  /// <param name="keyEventArgs">Event Text Arguments</param>
  private void OnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs) {
    Key pressedKey = keyEventArgs.Key == Key.System ? keyEventArgs.SystemKey : keyEventArgs.Key;
    switch (pressedKey) {
      case Key.Space:
        AddToSecureString(" ");
        keyEventArgs.Handled = true;
        break;
      case Key.Back:
      case Key.Delete:
        if (SelectionLength > 0) {
          RemoveFromSecureString(SelectionStart, SelectionLength);
        } else if (pressedKey == Key.Delete && CaretIndex < Text.Length) {
          RemoveFromSecureString(CaretIndex, 1);
        } else if (pressedKey == Key.Back && CaretIndex > 0) {
          int caretIndex = CaretIndex;
          if (CaretIndex > 0 && CaretIndex < Text.Length)
            caretIndex = caretIndex - 1;
          RemoveFromSecureString(CaretIndex - 1, 1);
          CaretIndex = caretIndex;
        }

        keyEventArgs.Handled = true;
        break;
    }
  }

  /// <summary>
  ///   Method to add new text into SecureString and process visual output
  /// </summary>
  /// <param name="text">Text to be added</param>
  private void AddToSecureString(string text) {
    if (SelectionLength > 0) {
      RemoveFromSecureString(SelectionStart, SelectionLength);
    }

    foreach (char c in text) {
      int caretIndex = CaretIndex;
      Password.InsertAt(caretIndex, c);
      MaskAllDisplayText();
      if (caretIndex == Text.Length) {
        _maskTimer.Stop();
        _maskTimer.Start();
        Text = Text.Insert(caretIndex++, c.ToString());
      } else {
        Text = Text.Insert(caretIndex++, "*");
      }
      CaretIndex = caretIndex;
    }
  }

  /// <summary>
  ///   Method to remove text from SecureString and process visual output
  /// </summary>
  /// <param name="startIndex">Start Position for Remove</param>
  /// <param name="trimLength">Length of Text to be removed</param>
  private void RemoveFromSecureString(int startIndex, int trimLength) {
    int caretIndex = CaretIndex;
    for (int i = 0; i < trimLength; ++i) {
      Password.RemoveAt(startIndex);
    }

    Text = Text.Remove(startIndex, trimLength);
    CaretIndex = caretIndex;
  }

  private void MaskAllDisplayText() {
    _maskTimer.Stop();
    int caretIndex = CaretIndex;
    Text = new string('*', Text.Length);
    CaretIndex = caretIndex;
  }
  #endregion
}

示例:

下载链接

您可以在控件中输入一些内容,并检查下方显示的存储值。

在此示例中,我添加了一个新的类型为string的DP,仅用于展示该控件的正常运行。在实际代码中,您显然不希望拥有该 DP(HiddenText),但我希望该示例能够帮助分析这个类是否真正起作用 :)


哇,谢谢 Viv!这正是我在寻找的类型!感谢您帮助一个 WPF 新手。 - aceinthehole
由于某种原因,我无法绑定“Password”-DP。它根本不会更新。SecureString不能绑定吗?我在应用程序中的任何地方都使用SecureString,直到我必须将其存储到文件中。然后,我暂时获取字符串并立即使用定义的密钥加密它。读取值时也是如此。因此,我认为SecureString非常适合这种设计。但是我无法使用绑定更新它 =/ - ecth
虽然通常情况下点赞就足够了,但这里不是。这是非常出色的工作!我建议添加NIST推荐的“显示密码”功能。 - jpwkeeper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接