H5W3
当前位置:H5W3 > 其他技术问题 > 正文

Java 编程语言中的口令屏蔽

  目前,对于 Java 命令行基于文本的输入/输出 API 的批评之一就是它缺乏对命令行输入口令屏蔽的支持。如果借助 AWT/Swing,这便不再成为问题,因为 AWT/Swing 提供了可以提供屏蔽口令的方法。

  2002 年 9 月,我发表了本文的早期版本,其后便不断收到大量感谢信、建设性的意见和在应用程序中使用源代码的许可。本文:

  概述了口令屏蔽  描述了用于口令屏蔽的   AWT/Swing 实用程序  为命令行输入口令屏蔽问题  提供独立于平台的解决方案  为口令屏蔽提供一个改进的解决方案(可靠而安全)

  口令屏蔽

  登录屏幕和登录对话框使用口令屏蔽技术,这种技术要么在输入口令时隐藏口令,要么显示一个字符(比如星号’*’)来代替用户输入的字符。例如,当您在一台 Windows 机器上进行登录时,一个登录对话框将会呈现在您眼前,其中的口令一栏使用星号作为屏蔽或回显字符。

  如果操作系统是 UNIX,则登录屏幕中的口令栏不显示回显字符。它的做法很简单,就是什么都不显示,

  AWT/Swing 中的口令屏蔽

  如果您希望为您的应用程序提供图形化的登录对话框,您可以使用 AWT 的 TextField 类,该类是一个文本组件,允许编辑单行文本。为了屏蔽口令栏,要使用 setEchoChar 方法。例如,为了把回显字符设置为星号,您需要这样做:

TextField password = new TextField(8);password.setEchoChar(‘*’);

  基于所使用字体的平均字符宽度,数字8指定了文本栏的宽度。您可以把回显字符设置为任何您喜欢的字符。注意,如果您把它设置为0,这意味着输入将会被回显,而不会被屏蔽。

  在 Swing 中,您可以使用 JPasswordField,它允许编辑单行文本,视图表明正在输入内容,但是不会显示原始字符。JPasswordField 类与和 setEchoChar 一起使用的 AWT 的 TextField 是源代码兼容的。如果您使用 JPasswordField, 默认的回显字符是星号 ‘*’, 但是您可以将其修改为任何您选定的字符。此外,如果您把回显字符设置为 0,这意味着字符将在输入时显示出来,而不会被屏蔽。图 2 显示了一个 Swing 登录对话框,其中的回显字符被设置为 #,使用的是下面的代码片断:

JPasswordField password = new JPasswordField(8);password.setEchoChar(‘#’);

  命令行输入屏蔽

  和 AWT/Swing 不同,在 Java 中没有特殊的 API 可用于屏蔽命令行输入。这也是许多开发人员一直所要求的一项功能。如果您希望为命令行基于文本的 Java 应用程序以及服务器端 Java 应用程序提供一个登录屏幕,它就很有用。提供这种功能的一种方式就是使用 Java 本地接口(Java Native Interface ,JNI)。对于不了解 C/C++ 或者希望坚持 100% 纯 Java 代码的某些 Java 开发人员来说,这可能有一定难度。

  这里我针对这个问题提出一个解决方案。在本文的早期版本中,所使用的是一个 UNIX 风格的登录屏幕,口令根本不在屏幕上回显。这样做的具体方法是,让一个单独的线程通过重写和打印口令提示命令行,尝试擦除回显到控制台的字符。大家仍然可以从论坛下载该篇文章中专用的代码和改进后的代码。

  然而,大家最需要的功能之一是使用星号"*"替换回显的字符。因此,本文从为口令屏蔽提供一个简单的解决方案开始,接着给出改进后的、更加可靠和安全的代码。

简单的解决方案

  这个解决方案使用一个单独的线程,在输入回显字符的时候擦除它们,然后使用星号代替它们。这是使用 EraserThread 类来完成的,如代码示例 1 所示。

代码示例 1:EraserThread.java

import java.io.*;

class EraserThread implements Runnable {   private boolean stop;

   /**    *@param The prompt displayed to the user    */   public EraserThread(String prompt) {       System.out.print(prompt);   }

   /**    * Begin masking…display asterisks (*)    */   public void run () {      stop = true;      while (stop) {         System.out.print("\010*");         try {            Thread.currentThread().sleep(1);         } catch(InterruptedException ie) {            ie.printStackTrace();         }      }   }

   /**    * Instruct the thread to stop masking    */   public void stopMasking() {      this.stop = false;   }}

  注意: 这个解决方案广泛利用了线程,然而,如果机器负载很重,就不能确保 MaskingThread 能够足够经常地运行。请继续阅读本文的余下部分来了解代码的改进版本。

PasswordField 类使用了 EraserThread 类,这一点在代码示例 2 中体现出来了。这个类提示用户输入口令,而且 EraserThread 的一个实例尝试使用 "*" 屏蔽输入。注意,一开始将显示一个星号 (*)。

  代码示例 2:PasswordField.java

public class PasswordField {

   /**    *@param prompt The prompt to display to the user    *@return The password as entered by the user    */   public static String readPassword (String prompt) {      EraserThread et = new EraserThread(prompt);      Thread mask = new Thread(et);      mask.start();

      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));      String password = "";

      try {         password = in.readLine();      } catch (IOException ioe) {        ioe.printStackTrace();      }      // stop masking      et.stopMasking();      // return the password entered by the user      return password;   }}

  作为如何使用 PasswordField 类的一个例子,考虑应用程序 TestApp,如示例代码 3 所示。这个应用程序显示一条提示,并等待用户输入口令。当然,输入被屏蔽为星号(*)。

  代码示例 3:TestApp.java

class TestApp {   public static void main(String argv[]) {      String password = PasswordField.readPassword("Enter password: ");      System.out.println("The password entered is: "+password);   }}

  如果您在 Windows、MacOS 或 UNIX 操作系统上运行 TesApp, 您将会发现其输出与图 3 类似。此外还要注意,当您运行该应用程序时,会显示一个初始的星号。

使代码安全而可靠

  上述的简单解决方案有一个主要缺陷:不应该使用字符串来存储诸如口令这类敏感信息!在本文的余下部分中,将会给出一个经过改进的解决方案。

  然而,首先,MaskingThread 类能够从几处改进中获益:

  为了确保跨线程的可见性,尤其是在多 CPU 的机器上,stop 字段应该被标记为 volatile.volatile 关键字指定同步线程使用该字段,这样编译器就不会对它进行任何优化;换句话说,应该从内存读取变量的值,而不应该在堆栈中保存任何拷贝。

  为了确保屏蔽能够在系统高负荷运转时也能够出现,在调用持续期间,调用线程的优先权被设定为最大。返回时再恢复其原始的优先权。

  代码示例 4 显示了修订后的 MaskingThread 类,修改的地方均以粗体形式突出显示。

import java.io.*;

/*** This class attempts to erase characters echoed to the console.*/

class MaskingThread extends Thread {   private volatile boolean stop;   private char echochar = ‘*’;

  /**   *@param prompt The prompt displayed to the user   */   public MaskingThread(String prompt) {      System.out.print(prompt);   }

  /**   * Begin masking until asked to stop.   */   public void run() {

      int priority = Thread.currentThread().getPriority();      Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

      try {         stop = true;         while(stop) {           System.out.print("\010" + echochar);           try {              // attempt masking at this rate              Thread.currentThread().sleep(1);           }catch (InterruptedException iex) {              Thread.currentThread().interrupt();              return;           }         }      } finally { // restore the original priority         Thread.currentThread().setPriority(priority);      }   }

  /**   * Instruct the thread to stop masking.   */   public void stopMasking() {      this.stop = false;   }}

尽管使用 Strings 收集和存储口令看起来似乎很合逻辑,它们并不适合存储诸如口令这样的敏感信息。这是因为 Strings 类型的对象是不可改变的——使用后不能重写或修改字符串的内容。应该使用一个 chars 数组作为代替。修订后的 PasswordField 如代码示例 5 所示,它是根据 Using Password-Based Encryption 改写而来。

  代码示例 5:PasswordField.java

import java.io.*;import java.util.*;

/*** This class prompts the user for a password and attempts to mask input with "*"*/

public class PasswordField {

  /**   *@param input stream to be used (e.g. System.in)   *@param prompt The prompt to display to the user.   *@return The password as entered by the user.   */

  public static final char[] getPassword(InputStream in, String prompt)    throws IOException {

      MaskingThread maskingthread = new MaskingThread(prompt);      Thread thread = new Thread(maskingthread);      thread.start();

      char[] lineBuffer;      char[] buf;      int i;

      buf = lineBuffer = new char[128];

      int room = buf.length;      int offset = 0;      int c;

      loop:   while (true) {         switch (c = in.read()) {            case -1:            case ”:               break loop;

            case ‘\r’:               int c2 = in.read();               if ((c2 != ”) && (c2 != -1)) {                  if (!(in instanceof PushbackInputStream)) {                     in = new PushbackInputStream(in);                  }                  ((PushbackInputStream)in).unread(c2);                } else {                  break loop;                }

                default:                   if (–room < 0) {                      buf = new char[offset + 128];                      room = buf.length – offset – 1;                      System.arraycopy(lineBuffer, 0, buf, 0, offset);                      Arrays.fill(lineBuffer, ‘ ‘);                      lineBuffer = buf;                   }                   buf[offset++] = (char) c;                   break;         }      }      maskingthread.stopMasking();      if (offset == 0) {         return null;      }      char[] ret = new char[offset];      System.arraycopy(buf, 0, ret, 0, offset);      Arrays.fill(buf, ‘ ‘);      return ret;   }}

  最后,PasswordApp 类如代码示例 6 所示,它只是一个用于测试修订后代码的测试应用程序。

  代码示例 6:PasswordApp.java

import java.io.*;

public class PasswordApp {   public static void main(String argv[]) {      char password[] = null;      try {         password = PasswordField.getPassword(System.in, "Enter your password: ");      } catch(IOException ioe) {         ioe.printStackTrace();      }      if(password == null ) {         System.out.println("No password entered");      } else {         System.out.println("The password entered is: "+String.valueOf(password));      }   }}

本文地址:H5W3 » Java 编程语言中的口令屏蔽

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址