为什么在密码中char[]比String更受欢迎?

3836
在Swing中,密码字段具有getPassword()方法(返回char[]),而不是通常的getText()方法(返回String)。同样,我发现建议不要使用String来处理密码。
为什么String在涉及密码时会对安全构成威胁? 使用char[]感觉很不方便。
17个回答

32

字符串是不可变的,一旦创建就不能改变。将密码作为字符串创建会在堆上或字符串池中留下与密码相关的引用。如果有人获取Java进程的堆转储并仔细扫描,他可能能够猜出密码。当然,这些未使用的字符串会被垃圾收集器回收,但这取决于何时GC启动。

另一方面,char[]是可变的,只要认证完成,您就可以用任何字符覆盖它们,例如所有M或反斜杠。现在,即使有人获取了堆转储,也可能无法获得当前未使用的密码。这在某种意义上为您提供了更多控制,例如自己清除对象内容而不是等待GC来执行。


只有在相关的JVM版本大于1.6时,它们才会被垃圾回收。在1.7之前,所有字符串都存储在permgen中。 - avgvstvs
2
@avgvstvs:“所有字符串都存储在permgen中”是完全错误的。只有被interned的字符串才会存储在那里,如果它们不是来自代码引用的字符串字面量,它们仍然会被垃圾回收。想一想,如果在JVM 1.7之前的JVM中通常从不GC字符串,那么任何Java应用程序如何能够存活超过几分钟呢? - Holger
@Holger 这是错误的。在1.7之前,被池化的stringsString池(先前使用过的字符串池)都存储在Permgen中。此外,请参见5.1节:https://docs.oracle.com/javase/specs/jvms/se6/html/ConstantPool.doc.html#67960JVM始终检查Strings是否具有相同的引用值,并会为您调用String.intern()。结果是每当JVM检测到常量池或堆中的相同字符串时,它都会将它们移动到Permgen中。我曾经在几个应用程序上遇到了“creeping permgen”问题,直到1.7版本。这是一个真正的问题。 - avgvstvs
因此,简要概括一下:在1.7版本之前,字符串开始位于堆中,当它们被使用时,它们会被放入constant_pool中,该池位于permgen中。如果一个字符串被多次使用,它将被放到字符串池中。 - avgvstvs
2
@avgvstvs:不存在“先前使用的字符串池”。你正在将完全不同的东西混在一起。有一个运行时字符串池,其中包含字符串文字和明确内部化的字符串,但没有其他内容。每个类都有其常量池,其中包含编译时常量。这些字符串会自动添加到运行时池中,但仅限于这些,而不是每个字符串。 - Holger

29

字符串是不可变的并且进入字符串池。一旦写入,就无法被覆盖。

char[] 是一个数组,你应该在使用密码后覆盖它,以下是应该这样做的方法:

char[] passw = request.getPassword().toCharArray()
if (comparePasswords(dbPassword, passw) {
  allowUser = true;
  cleanPassword(passw);
  cleanPassword(dbPassword);
  passw = null;
}

private static void cleanPassword (char[] pass) {

  Arrays.fill(pass, '0');
}

攻击者可能使用的一个情景是崩溃转储 - 当JVM崩溃并生成内存转储时,您将能够看到密码。

这不一定是恶意的外部攻击者。这可能是一个具有监控服务器访问权限的支持用户。他/她可以窥视崩溃转储并找到密码。


2
ch = null; 你不能这样做。 - Yugerten
3
request.getPassword()不是已经创建字符串并将其添加到池中了吗? - Tvde1
2
ch = '0' 改变了局部变量 ch,但对数组没有影响。而且你的例子本来就毫无意义,你首先使用一个字符串实例调用 toCharArray() 方法,创建一个新的数组,即使你正确地覆盖了新数组,它也不会改变原始的字符串实例,所以与直接使用字符串实例相比并没有任何优势。 - Holger
@Holger 谢谢。已经修正了字符数组清理代码。 - ACV
3
你可以简单地使用Arrays.fill(pass, '0'); - Holger

25

简短明了的答案是,char[] 是可变的,而 String 对象是不可变的。

在 Java 中,String 是不可变对象。这就是为什么一旦创建后它们就无法被修改,因此从内存中删除它们的唯一方法是进行垃圾回收。只有当对象释放的内存可以被覆盖时,数据才会消失。

现在 Java 中的垃圾回收并没有发生在任何保证间隔。因此,String 可以在内存中存在很长时间,如果在此期间进程崩溃,则字符串的内容可能会出现在内存转储或某个日志中。

使用字符数组,您可以读取密码,尽快处理完毕,然后立即更改内容。


@fallenidol 一点也不。仔细阅读,你会发现差异。 - Pritam Banerjee

15

案例字符串:

    String password = "ill stay in StringPool after Death !!!";
    // some long code goes
    // ...Now I want to remove traces of password
    password = null;
    password = "";
    // above attempts wil change value of password
    // but the actual password can be traced from String pool through memory dump, if not garbage collected

情况字符数组:

    char[] passArray = {'p','a','s','s','w','o','r','d'};
    // some long code goes
    // ...Now I want to remove traces of password
    for (int i=0; i<passArray.length;i++){
        passArray[i] = 'x';
    }
    // Now you ACTUALLY DESTROYED traces of password form memory

13

Java中的字符串是不可变的。因此,每当创建字符串时,它将一直保留在内存中,直到垃圾收集器对其进行回收。因此,任何可以访问内存的人都可以读取字符串的值。

如果修改字符串的值,则会创建一个新的字符串。因此,原始值和修改后的值都会保留在内存中,直到被垃圾收集器回收。

使用字符数组,一旦密码的目的得到服务,即可修改或擦除数组内容。在修改之后甚至在垃圾收集开始之前,原始数组内容将不会在内存中找到。

由于安全问题,最好将密码存储为字符数组。


3
这个问题是有争议的,因为使用String和Char[]都有其优缺点。这取决于用户的需求。
由于Java中的String是不可变的,每当有人试图操作您的字符串时,它就会创建一个新对象,现有的String保持不变。这可以看作是将密码存储为String的优点之一,但即使在使用后,该对象仍然存在于内存中。因此,如果有人以某种方式获取了对象的内存位置,那么该人就可以轻松地追踪存储在该位置上的密码。
Char[]是可变的,但它具有的优点是,在使用后,程序员可以显式清除数组或覆盖值。因此,当使用完毕时,它会被清理,没有人能够知道您存储的信息。
基于上述情况,可以得出一个想法,无论是选择String还是Char[]来满足他们的需求。

1
许多之前的回答都很好。我认为还有一个点(如果我错了,请纠正我)。
Java默认使用UTF-16存储字符串。使用字符数组可以方便地使用Unicode、区域字符等。这种技术允许所有字符集平等地被尊重,以存储密码,并且不会由于字符集混淆而引发某些加密问题。最后,使用字符数组,我们可以将密码数组转换为所需的字符集字符串。

char在Java中是一个16位的值,char数组也被假定为UTF-16编码。https://docs.oracle.com/javase/7/docs/api/java/lang/Character.html#:~:text=Java平台在char数组中使用UTF-16表示。如果你想要存储不同编码的字符串,我认为你必须使用字节数组。要获取不同编码的字符串,可以使用String.getBytes方法。 - JayK

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