目前对于 Java 命令行基于文本的输入/输出 API 的批评之一就是它缺乏对命令行输入口令屏蔽的支持如果借助 AWT/Swing这便不再成为问题因为 AWT/Swing 提供了可以提供屏蔽口令的方法
年 月我发表了本文的早期版本其后便不断收到大量感谢信建设性的意见和在应用程序中使用源代码的许可本文
概述了口令屏蔽
描述了用于口令屏蔽的
AWT/Swing 实用程序
为命令行输入口令屏蔽问题
提供独立于平台的解决方案
为口令屏蔽提供一个改进的解决方案(可靠而安全)
口令屏蔽
登录屏幕和登录对话框使用口令屏蔽技术这种技术要么在输入口令时隐藏口令要么显示一个字符(比如星号*)来代替用户输入的字符例如当您在一台 Windows 机器上进行登录时一个登录对话框将会呈现在您眼前其中的口令一栏使用星号作为屏蔽或回显字符
如果操作系统是 UNIX则登录屏幕中的口令栏不显示回显字符它的做法很简单就是什么都不显示
AWT/Swing 中的口令屏蔽
如果您希望为您的应用程序提供图形化的登录对话框您可以使用 AWT 的 TextField 类该类是一个文本组件允许编辑单行文本为了屏蔽口令栏要使用 setEchoChar 方法例如为了把回显字符设置为星号您需要这样做
TextField password = new TextField();
passwordsetEchoChar(*);
基于所使用字体的平均字符宽度数字指定了文本栏的宽度您可以把回显字符设置为任何您喜欢的字符注意如果您把它设置为这意味着输入将会被回显而不会被屏蔽
在 Swing 中您可以使用 JPasswordField它允许编辑单行文本视图表明正在输入内容但是不会显示原始字符JPasswordField 类与和 setEchoChar 一起使用的 AWT 的 TextField 是源代码兼容的如果您使用 JPasswordField 默认的回显字符是星号 * 但是您可以将其修改为任何您选定的字符此外如果您把回显字符设置为 这意味着字符将在输入时显示出来而不会被屏蔽图 显示了一个 Swing 登录对话框其中的回显字符被设置为 #使用的是下面的代码片断
JPasswordField password = new JPasswordField();
passwordsetEchoChar(#);
命令行输入屏蔽
和 AWT/Swing 不同在 Java 中没有特殊的 API 可用于屏蔽命令行输入这也是许多开发人员一直所要求的一项功能如果您希望为命令行基于文本的 Java 应用程序以及服务器端 Java 应用程序提供一个登录屏幕它就很有用提供这种功能的一种方式就是使用 Java 本地接口(Java Native Interface JNI)对于不了解 C/C++ 或者希望坚持 % 纯 Java 代码的某些 Java 开发人员来说这可能有一定难度
这里我针对这个问题提出一个解决方案在本文的早期版本中所使用的是一个 UNIX 风格的登录屏幕口令根本不在屏幕上回显这样做的具体方法是让一个单独的线程通过重写和打印口令提示命令行尝试擦除回显到控制台的字符大家仍然可以从论坛下载该篇文章中专用的代码和改进后的代码
然而大家最需要的功能之一是使用星号*替换回显的字符因此本文从为口令屏蔽提供一个简单的解决方案开始接着给出改进后的更加可靠和安全的代码
简单的解决方案
这个解决方案使用一个单独的线程在输入回显字符的时候擦除它们然后使用星号代替它们这是使用 EraserThread 类来完成的如代码示例 所示
代码示例 EraserThreadjava
import javaio*;
class EraserThread implements Runnable {
private boolean stop;
/**
*@param The prompt displayed to the user
*/
public EraserThread(String prompt) {
Systemoutprint(prompt);
}
/**
* Begin maskingdisplay asterisks (*)
*/
public void run () {
stop = true;
while (stop) {
Systemoutprint(\*);
try {
ThreadcurrentThread()sleep();
} catch(InterruptedException ie) {
ieprintStackTrace();
}
}
}
/**
* Instruct the thread to stop masking
*/
public void stopMasking() {
thisstop = false;
}
}
注意 这个解决方案广泛利用了线程然而如果机器负载很重就不能确保 MaskingThread 能够足够经常地运行请继续阅读本文的余下部分来了解代码的改进版本
PasswordField 类使用了 EraserThread 类这一点在代码示例 中体现出来了这个类提示用户输入口令而且 EraserThread 的一个实例尝试使用 * 屏蔽输入注意一开始将显示一个星号 (*)
代码示例 PasswordFieldjava
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);
maskstart();
BufferedReader in = new BufferedReader(new InputStreamReader(Systemin));
String password = ;
try {
password = inreadLine();
} catch (IOException ioe) {
ioeprintStackTrace();
}
// stop masking
etstopMasking();
// return the password entered by the user
return password;
}
}
作为如何使用 PasswordField 类的一个例子考虑应用程序 TestApp如示例代码 所示这个应用程序显示一条提示并等待用户输入口令当然输入被屏蔽为星号(*)
代码示例 TestAppjava
class TestApp {
public static void main(String argv[]) {
String password = PasswordFieldreadPassword(Enter password: );
Systemoutprintln(The password entered is: +password);
}
}
如果您在 WindowsMacOS 或 UNIX 操作系统上运行 TesApp 您将会发现其输出与图 类似此外还要注意当您运行该应用程序时会显示一个初始的星号
使代码安全而可靠
上述的简单解决方案有一个主要缺陷不应该使用字符串来存储诸如口令这类敏感信息!在本文的余下部分中将会给出一个经过改进的解决方案
然而首先MaskingThread 类能够从几处改进中获益
为了确保跨线程的可见性尤其是在多 CPU 的机器上stop 字段应该被标记为 volatilevolatile 关键字指定同步线程使用该字段这样编译器就不会对它进行任何优化换句话说应该从内存读取变量的值而不应该在堆栈中保存任何拷贝
为了确保屏蔽能够在系统高负荷运转时也能够出现在调用持续期间调用线程的优先权被设定为最大返回时再恢复其原始的优先权
代码示例 显示了修订后的 MaskingThread 类修改的地方均以粗体形式突出显示
import javaio*;
/**
* 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) {
Systemoutprint(prompt);
}
/**
* Begin masking until asked to stop
*/
public void run() {
int priority = ThreadcurrentThread()getPriority();
ThreadcurrentThread()setPriority(ThreadMAX_PRIORITY);
try {
stop = true;
while(stop) {
Systemoutprint(\ + echochar);
try {
// attempt masking at this rate
ThreadcurrentThread()sleep();
}catch (InterruptedException iex) {
ThreadcurrentThread()interrupt();
return;
}
}
} finally { // restore the original priority
ThreadcurrentThread()setPriority(priority);
}
}
/**
* Instruct the thread to stop masking
*/
public void stopMasking() {
thisstop = false;
}
}
尽管使用 Strings 收集和存储口令看起来似乎很合逻辑它们并不适合存储诸如口令这样的敏感信息这是因为 Strings 类型的对象是不可改变的——使用后不能重写或修改字符串的内容应该使用一个 chars 数组作为代替修订后的 PasswordField 如代码示例 所示它是根据 Using PasswordBased Encryption 改写而来
代码示例 PasswordFieldjava
import javaio*;
import javautil*;
/**
* This class prompts the user for a password and attempts to mask input with *
*/
public class PasswordField {
/**
*@param input stream to be used (eg Systemin)
*@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);
threadstart();
char[] lineBuffer;
char[] buf;
int i;
buf = lineBuffer = new char[];
int room = buflength;
int offset = ;
int c;
loop: while (true) {
switch (c = inread()) {
case :
case :
break loop;
case \r:
int c = inread();
if ((c != ) && (c != )) {
if (!(in instanceof PushbackInputStream)) {
in = new PushbackInputStream(in);
}
((PushbackInputStream)in)unread(c);
} else {
break loop;
}
default:
if (room < ) {
buf = new char[offset + ];
room = buflength offset ;
Systemarraycopy(lineBuffer buf offset);
Arraysfill(lineBuffer );
lineBuffer = buf;
}
buf[offset++] = (char) c;
break;
}
}
maskingthreadstopMasking();
if (offset == ) {
return null;
}
char[] ret = new char[offset];
Systemarraycopy(buf ret offset);
Arraysfill(buf );
return ret;
}
}
最后PasswordApp 类如代码示例 所示它只是一个用于测试修订后代码的测试应用程序
代码示例 PasswordAppjava
import javaio*;
public class PasswordApp {
public static void main(String argv[]) {
char password[] = null;
try {
password = PasswordFieldgetPassword(Systemin Enter your password: );
} catch(IOException ioe) {
ioeprintStackTrace();
}
if(password == null ) {
Systemoutprintln(No password entered);
} else {
Systemoutprintln(The password entered is: +StringvalueOf(password));
}
}
}