Java 使用 char[] Array 还是 String 存储字符串密码

概述

在本文章中,我们主要用来说明为什么应该使用 char[] 数组来存储密码,而不是使用 String 来存储密码。

需要注意的是,为了密码的安全,我们通常都会将用户输入的密码 MD5 加密哈希后进行存储。

我们通常是不会在后台中存储明文的用户密码的,这篇文章主要目的就是为了说明字符串在 Java 中的存储方式和在存储中的实现,就算你应该使用 char[] 数组来存储,你也不应该在程序中使用明文。

同时,本文章还假设你没有办法对 String 字符串进行控制。例如你获得密码是从某些第三方工具上面获得的,或者第三方 API 传递过来的,通常你是没有办法对上面的字符串进行控制的。

因此,你还不得不使用 java.lang.String 对象来对密码进行实现,经过 Java 的官方小组还是推荐使用 char[] 数组来实现。

你可以通过单击 JPasswordField 这个链接来查看 JPasswordField API 的使用,这个 API 是存在 javax.swing 包中的。

我们可以知道 getText() 这个返回 String 的方法从 Java 2 开始就被丢弃了,你应该使用 getPassword() 来返回密码,这个方法实际上是返回的 char[] 字符串。

下面来让我们看看为什么应该使用 char[] 数组来存储密码了。

Strings 是不可变的(Immutable)

String 在 Java 中是不可变的。这个不可变的意思是,String 是不能被更高一级的 API 进行操作的。

任何对 String 对象的修改都会创建一个新的 String 对象,同时将老的 String 对象保存在内存中。

上面这句话的意思就是:如果密码(Password)使用 String 来进行存储的话,如果你对密码进行操作后,老的密码还是在内存中存在的,知道 Java 的垃圾回收程序来清理掉。

这个垃圾回收的过程,我们是没有办法进行控制的,我们也不知道 JVM 什么时候执行垃圾清理。这个清理的过程与其他对象的清理对比来说,可能需要等待比较长的时间。这是因为 String 在 JVM 中是存储在 String Pool 中的,这个主要是为了便于对 String 的再次利用。

因为有这个缓存的存在,所以 String 在内存中保留的时间会比较长。

在这个过程中,任何人如果对 JVM 进行 Dump 内存操作的话,任何人都可以从内存中获得密码的明文。

如果我们使用 char[] 数组来存储密码的话,我们可以在对密码的计算完成后来使用程序对数组进行清理。因此,我们可以保证我们使用过的密码从内存中完全清楚,而不是等候 JVM 垃圾清理程序来进行清理。

下面我们来看代码来对上面的用例进行说明:

String 测试

    @Test
    public void immutableForString() {
        String stringPassword = "password";
        System.out.print("Original String password value: ");
        System.out.println(stringPassword);
        System.out.println("Original String password hashCode: " + Integer.toHexString(stringPassword.hashCode()));

        String newString = "********";
        stringPassword.replace(stringPassword, newString);

        System.out.print("String password value after trying to replace it: ");
        System.out.println(stringPassword);
        System.out.println("hashCode after trying to replace the original String: " + Integer.toHexString(stringPassword.hashCode()));
    }

上面程序将会有如下输出:

Original String password value: password
Original String password hashCode: 4889ba9b
String password value after trying to replace it: password
hashCode after trying to replace the original String: 4889ba9b

char 数组测试

    @Test
    public void immutableForCharArray() {
        char[] charPassword = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};

        System.out.print("Original char password value: ");
        System.out.println(charPassword);
        System.out.println("Original char password hashCode: " + Integer.toHexString(charPassword.hashCode()));

        Arrays.fill(charPassword, '*');

        System.out.print("Changed char password value: ");
        System.out.println(charPassword);
        System.out.println("Changed char password hashCode: " + Integer.toHexString(charPassword.hashCode()));
    }

针对数组的测试输出如下:

Original char password value: password
Original char password hashCode: 10d59286
Changed char password value: ********
Changed char password hashCode: 10d59286

正如我们所看到的那样,当我们对 String 进行了操作后,操作后的结果是不会改变原始输入 String 的结果的。

我们可以看到上面的代码,hashCode() 方法返回的结果是一样的,并没有给我们有不同的结果,同时 String 中的值也保持一致。

使用 char[] 数组的时候,我们注意到,hashCode() 的值是一样的,但是内容却不一样了。这是因为我们对 char[] 进行了操作所导致的,我们可以对相同的对象中的数据进行修改。

需要注意 stringPassword.replace(stringPassword, newString); 方法,如果你需要获得这个方法替换后的值得话,你需要将方法执行后的值重新赋值才可以。

避免意外打印密码

使用 char[] 数组来存储密码的好处就是能够避免意外的将内存中存储的密码数据输出到控制台,显示器或者其他并不安全的地方。

让我们来考察下面的代码:

    @Test
    public void accidentallyPassword_print() {
        String passwordString = "password";
        char[] passwordArray = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        System.out.println("Printing String password -> " + passwordString);
        System.out.println("Printing char[] password -> " + passwordArray);
    }

上面的代码将会输出为:

Printing String password -> password
Printing char[] password -> [[email protected]

我们可以从上面的输出了解到,String 的输出是完整的内容输出,char[] 的输出不是将 char[] 中的内容输出,这样的方式让输出更不容易泄密。

这是因为在 Char 数组打印的时候调用的是一个 toString 的方法,这个方法输出的是类的值和类的哈希代码(hashCode)转换成 16 进制。

这就是你看到这一串奇怪字符串的原因。

结论

在这篇文章中,我们对为什么应该使用 char 数组而不是使用 String 来存储密码或者敏感字符串的原因进行了说明。

同时通过举例来说明了一些相关问题和结构。

测试源代码

相关的测试源代码,请访问链接:

您也可以 Fork 代码后提交更新。