ChatGPT解决这个技术问题 Extra ChatGPT

为什么 char[] 优于 String 的密码?

在 Swing 中,密码字段有一个 getPassword()(返回 char[])方法,而不是通常的 getText()(返回 String)方法。同样,我遇到了一个建议,不要使用 String 来处理密码。

为什么在密码方面 String 会对安全构成威胁?使用char[]感觉不方便。


N
Neuron

字符串是不可变的。这意味着一旦您创建了 String,如果另一个进程可以转储内存,那么(除了 reflection)您将无法在 garbage collection 启动之前删除数据。

使用数组,您可以在完成后显式擦除数据。你可以用你喜欢的任何东西覆盖数组,密码不会出现在系统的任何地方,甚至在垃圾收集之前。

所以是的,这一个安全问题 - 但即使使用 char[] 也只会减少攻击者的机会之窗,而且它仅适用于这种特定类型的攻击。

如评论中所述,垃圾收集器移动的数组可能会将数据的杂散副本留在内存中。我相信这是特定于实现的 - 垃圾收集器可能清除所有内存,以避免这种事情。即使是这样,仍有一段时间 char[] 包含实际字符作为攻击窗口。


如果一个进程可以访问您的应用程序的内存,那么这已经是一个安全漏洞,对吧?
@Yeti:是的,但它不像是黑白的。如果他们只能获取内存的快照,那么您希望减少快照可以造成的损害,或者减少可以拍摄非常严重的快照的窗口。
一种常见的攻击方法是运行一个分配大量内存的进程,然后扫描它以查找剩余的有用数据,例如密码。该进程不需要对另一个进程的内存空间进行任何神奇的访问;它只是依赖于其他进程在没有首先清除敏感数据的情况下死亡,并且操作系统在使其可用于新进程之前也不会清除内存(或页面缓冲区)。清除存储在 char[] 位置的密码会切断该攻击线,而使用 String 则无法做到这一点。
如果操作系统在将内存交给另一个进程之前没有清除内存,则操作系统存在重大安全问题!然而,从技术上讲,清除通常是通过保护模式技巧完成的,如果 CPU 坏了(例如 Intel Meldown),仍然可以读取旧的内存内容。
@PrateekPande:只有在源代码中存在或显式实习时,它才会出现在文字池中。总的来说,这两个都是坏主意……
K
Kyle Rosendo

虽然这里的其他建议似乎有效,但还有一个很好的理由。使用普通的 String,您不小心将密码打印到日志、监视器或其他一些不安全的地方的几率要高得多。 char[] 不太容易受到攻击。

考虑一下:

public static void main(String[] args) {
    Object pw = "Password";
    System.out.println("String: " + pw);

    pw = "Password".toCharArray();
    System.out.println("Array: " + pw);
}

印刷:

String: Password
Array: [C@5829428e

@voo,但我怀疑您是否会通过直接写入流和连接来登录。日志框架会将 char[] 转换为良好的输出
@Thr4wn toString 的默认实现是 classname@hashcode[C代表char[],其余为十六进制哈希码。
有趣的想法。我想指出,这不会转置为对数组具有有意义的 toString 的 Scala。
我会为此编写一个 Password 类类型。它不那么晦涩难懂,也更难意外通过某个地方。
为什么有人会假设 char 数组将被转换为对象?我不确定我明白为什么每个人都喜欢这个答案。假设你这样做了: System.out.println("Password".toCharArray());
B
Bruno

引用官方文档,Java Cryptography Architecture guide 是关于 char[]String 密码(关于基于密码的加密,但这当然更普遍地与密码有关):

将密码收集并存储在 java.lang.String 类型的对象中似乎是合乎逻辑的。但是,需要注意的是:String 类型的对象是不可变的,即没有定义允许您在使用后更改(覆盖)或清零 String 内容的方法。此功能使 String 对象不适合存储用户密码等安全敏感信息。您应该始终将安全敏感信息收集并存储在 char 数组中。

Guideline 2-2 of the Secure Coding Guidelines for the Java Programming Language, Version 4.0 也说了类似的话(尽管它最初是在日志记录的上下文中):

准则 2-2:不要记录高度敏感的信息 一些信息,例如社会安全号码 (SSN) 和密码,是高度敏感的。此信息不应保存超过必要的时间,也不应保存在可能被管理员看到的地方。例如,不应将其发送到日志文件,并且不应通过搜索检测到其存在。一些瞬态数据可能保存在可变数据结构中,例如 char 数组,并在使用后立即清除。清除数据结构在典型的 Java 运行时系统上降低了效率,因为对象在内存中对程序员透明地移动。该指南还对不具备所处理数据的语义知识的低级库的实现和使用产生影响。例如,低级字符串解析库可能会记录它处理的文本。应用程序可以使用库解析 SSN。这会产生一种情况,即 SSN 可供有权访问日志文件的管理员使用。


这正是我在乔恩的回答下面谈到的有缺陷/虚假的参考资料,这是一个众所周知的来源,有很多批评。
@bestass你能不能也引用一个参考?
@bestass 很抱歉,但是 String 很好理解,它在 JVM 中的行为方式......在以安全方式处理密码时,有充分的理由使用 char[] 代替 String
尽管密码作为字符串从浏览器传递给请求,但又是“字符串”而不是字符?所以无论你做什么,它都是一个字符串,此时它应该被处理并丢弃,而不是存储在内存中?
@Dawesi - At which point - 这是特定于应用程序的,但一般规则是一旦你拿到应该是密码的东西(明文或其他)就这样做。例如,您可以从浏览器获取它作为 HTTP 请求的一部分。你无法控制投递,但你可以控制自己的存储,所以一拿到它,就把它放在一个 char[] 中,用它做你需要做的事情,然后将所有设置为 '0' 并让 gc收回它。
P
Peter Mortensen

字符数组 (char[]) 可以在使用后通过将每个字符设置为零而不是字符串来清除。如果有人能以某种方式看到内存映像,如果使用字符串,他们可以看到纯文本密码,但如果使用 char[],在用 0 清除数据后,密码是安全的。


默认情况下不安全。如果我们谈论的是 Web 应用程序,大多数 Web 容器都会将密码以明文形式传递给 HttpServletRequest 对象。如果 JVM 版本是 1.6 或更低,它将在 permgen 空间中。如果它在 1.7 中,它在被收集之前仍然是可读的。 (无论何时。)
@avgvstvs:字符串不会自动移动到 permgen 空间,这只适用于实习字符串。除此之外,permgen 空间也受到垃圾收集的影响,只是速度较低。 permgen 空间的真正问题在于它的固定大小,这正是没有人应该在任意字符串上盲目调用 intern() 的原因。但是您是对的,因为 String 实例首先存在(直到收集),然后将它们转换为 char[] 数组并不会改变它。
@Holger 请参阅docs.oracle.com/javase/specs/jvms/se6/html/…“否则,将创建一个 String 类的新实例,其中包含 CONSTANT_String_info 结构给出的 Unicode 字符序列;该类实例是字符串文字推导的结果。最后,新 String 实例的 intern 方法被调用。”在 1.6 中,当 JVM 检测到相同的序列时,它会为您调用 intern。
@Holger,你是对的,我将常量池和字符串池混为一谈,但是 permgen 空间 only 应用于实习字符串也是错误的。在 1.7 之前,constant_pool 和 string_pool 都驻留在 permgen 空间中。这意味着分配给堆的唯一字符串类是如您所说的 new String()StringBuilder.toString() 我管理具有大量字符串常量的应用程序,因此我们有很多 permgen crawl。直到 1.7。
@avgvstvs:嗯,字符串常量,按照 JLS 的要求,总是被实习的,因此实习字符串最终在 permgen 空间中的语句,隐式应用于字符串常量。唯一的区别是字符串常量首先是在 permgen 空间中创建的,而对任意字符串调用 intern() 可能会导致在 permgen 空间中分配等效字符串。如果没有共享该对象的相同内容的文字字符串,则后者可能会被 GC 处理……
s
stites

有些人认为,一旦不再需要密码,就必须覆盖用于存储密码的内存。这减少了攻击者必须从您的系统读取密码的时间窗口,并且完全忽略了攻击者已经需要足够的访问权限来劫持 JVM 内存来执行此操作的事实。具有如此多访问权限的攻击者可以捕获您的关键事件,从而使其完全无用(AFAIK,如果我错了,请纠正我)。

更新

感谢评论,我必须更新我的答案。显然,在两种情况下,这可以增加(非常)小的安全性改进,因为它减少了密码可能落在硬盘上的时间。我仍然认为对于大多数用例来说这太过分了。

您的目标系统可能配置错误,或者您必须假设它是,并且您必须对核心转储持偏执态度(如果系统不是由管理员管理,则可能有效)。

您的软件必须过于偏执,以防止攻击者获得对硬件的访问权限而导致数据泄露——使用诸如 TrueCrypt(已停产)、VeraCrypt 或 CipherShed 之类的东西。

如果可能,禁用核心转储和交换文件将解决这两个问题。但是,它们需要管理员权限,并且可能会减少功能(使用更少的内存)并且从正在运行的系统中提取 RAM 仍然是一个有效的问题。


我会用“只是轻微的安全改进”来代替“完全没用”。例如,如果您碰巧拥有对 tmp 目录的读取权限、一台配置错误的机器以及您的应用程序崩溃,那么您就可以访问内存转储。在这种情况下,您将无法安装键盘记录器,但您可以分析核心转储。
完成后立即从内存中擦除未加密的数据被认为是最佳实践,而不是因为它是万无一失的(它不是);但因为它降低了你的威胁暴露水平。这样做并不能防止实时攻击;但是因为它通过显着减少在对内存快照的追溯攻击中暴露的数据量(例如,写入交换文件或从内存中读取的应用程序内存的副本)来提供损害缓解工具从正在运行的服务器并在其状态失败之前移动到另一个服务器)。
我倾向于同意这种回应的态度。我冒昧地提出,大多数安全漏洞的后果都发生在比内存中的位更高的抽象级别上。当然,在超安全防御系统中可能存在一些场景,这可能会引起相当大的关注,但在这个级别上认真考虑对于 99% 正在利用 .NET 或 Java 的应用程序来说是多余的(因为它与垃圾收集有关)。
在 Heartbleed 穿透服务器内存并泄露密码之后,我会将字符串“只是一个小的安全改进”替换为“绝对不能使用字符串作为密码,而是使用 char []。”
@PetervdL heartbleed 仅允许读取特定的重用缓冲区集合(用于安全关键数据和网络 I/O,而不会在两者之间清除 - 出于性能原因),您不能将其与 Java 字符串结合使用,因为它们在设计上不可重用.您也不能使用 Java 读入随机内存来获取字符串的内容。导致心脏出血的语言和设计问题对于 Java 字符串是不可能的。
S
Sean Owen

我不认为这是一个有效的建议,但是,我至少可以猜到原因。

我认为动机是希望确保您可以在使用后立即并确定地清除内存中的所有密码痕迹。使用 char[],您可以肯定地用空白或其他内容覆盖数组的每个元素。您不能以这种方式编辑 String 的内部值。

但这并不是一个好的答案。为什么不只是确保对 char[]String 的引用不会逃逸?那么就没有安全问题了。但问题是 String 对象在理论上可以intern() 编辑并在常量池中保持活动状态。我想使用 char[] 禁止这种可能性。


我不会说问题在于您的参考文献会或不会“逃脱”。只是字符串会在内存中保持一段时间不被修改,而 char[] 可以修改,然后它是否被收集无关紧要。而且由于需要为非文字显式地完成字符串实习,这就像告诉静态字段可以引用 char[] 一样。
内存中的密码不是作为表单帖子中的字符串吗?
e
emboss

答案已经给出,但我想分享一个我最近在 Java 标准库中发现的问题。虽然他们现在非常注意用 char[] 替换密码字符串(这当然是一件好事),但在从内存中清除其他安全关键数据时似乎被忽略了。

我正在考虑例如 PrivateKey 类。考虑一个场景,您将从 PKCS#12 文件加载私有 RSA 密钥,并使用它来执行某些操作。现在在这种情况下,只要适当限制对密钥文件的物理访问,单独嗅探密码对您没有多大帮助。作为攻击者,如果您直接获得密钥而不是密码,您的情况会好得多。所需的信息可以是多种泄漏、核心转储、调试器会话或交换文件只是一些示例。

事实证明,没有什么可以让您从内存中清除 PrivateKey 的私有信息,因为没有 API 可以让您擦除形成相应信息的字节。

这是一个糟糕的情况,因为此 paper 描述了如何可能利用这种情况。

例如,OpenSSL 库会在释放私钥之前覆盖关键内存部分。由于 Java 是垃圾收集的,因此我们需要明确的方法来擦除和无效 Java 密钥的私有信息,这些信息将在使用密钥后立即应用。


解决此问题的一种方法是使用 PrivateKey 的实现,它实际上不会将其私有内容加载到内存中:例如,通过 PKCS#11 硬件令牌。也许 PKCS#11 的软件实现可以手动清理内存。也许使用类似 NSS 存储(它与 Java 中的 PKCS11 存储类型共享其大部分实现)之类的东西会更好。 KeychainStore(OSX 密钥库)将私钥的全部内容加载到其 PrivateKey 实例中,但它不需要这样做。 (不确定 WINDOWS-MY KeyStore 在 Windows 上的作用。)
@Bruno 当然,基于硬件的令牌不会受到此影响,但是在您或多或少被迫使用软件密钥的情况下呢?并非每个部署都有预算来负担 HSM。软件密钥存储在某些时候必须将密钥加载到内存中,因此 IMO 我们至少应该可以选择再次随意清除内存。
当然,我只是想知道某些与 HSM 等效的软件实现在清理内存方面是否表现得更好。例如,当在 Safari/OSX 中使用 client-auth 时,Safari 进程实际上永远不会看到私钥,操作系统提供的底层 SSL 库直接与提示用户使用钥匙串中的密钥的安全守护进程对话。虽然这一切都是在软件中完成的,但如果将签名委托给可以更好地卸载或清除内存的不同实体(甚至是基于软件的),类似这样的分离可能会有所帮助。
@Bruno:有趣的想法,一个额外的负责清除内存的间接层确实可以透明地解决这个问题。为软件密钥库编写一个 PKCS#11 包装器已经可以解决问题了吗?
有趣的是,您应该说他们非常注意“现在”使用 char[],因为我正在查看 JDK 9 中添加的这个不错的新 ConnectionBuilder 类,它仍然有 password(String) 并且没有通过 {1 的选项}。似乎使用 Java 有很多“做我说的,而不是我做的”。
P
Peter Lawrey

正如 Jon Skeet 所说,除了使用反射之外别无他法。

但是,如果反射是您的选择,您可以这样做。

public static void main(String[] args) {
    System.out.println("please enter a password");
    // don't actually do this, this is an example only.
    Scanner in = new Scanner(System.in);
    String password = in.nextLine();
    usePassword(password);

    clearString(password);

    System.out.println("password: '" + password + "'");
}

private static void usePassword(String password) {

}

private static void clearString(String password) {
    try {
        Field value = String.class.getDeclaredField("value");
        value.setAccessible(true);
        char[] chars = (char[]) value.get(password);
        Arrays.fill(chars, '*');
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

运行时

please enter a password
hello world
password: '***********'

注意:如果字符串的 char[] 已作为 GC 循环的一部分被复制,则前一个副本有可能在内存中的某个位置。

这个旧副本不会出现在堆转储中,但如果您可以直接访问进程的原始内存,您可以看到它。一般来说,您应该避免任何拥有此类访问权限的人。


最好也采取一些措施来防止打印我们从 '***********' 获得的密码长度。
@chux,您可以使用零宽度字符,尽管这可能比有用更令人困惑。不使用 Unsafe 就无法更改 char 数组的长度。 ;)
由于 Java 8 的字符串重复数据删除,我认为这样做可能会非常具有破坏性……您最终可能会清除程序中偶然具有与密码字符串相同的值的其他字符串。不太可能,但可能...
@PeterLawrey 必须使用 JVM 参数启用它,但它就在那里。可以在这里阅读:blog.codecentric.de/en/2014/08/…
密码很有可能仍然在 Scanner 的内部缓冲区中,并且由于您没有使用 System.console().readPassword(),因此在控制台窗口中以可读的形式存在。但对于大多数实际用例,usePassword 的执行持续时间是实际问题。例如,当与另一台机器建立连接时,它会花费大量时间并告诉攻击者现在是在堆中搜索密码的正确时间。唯一的解决方案是防止攻击者读取堆内存……
M
Mihai Chelaru

这些都是原因,应该选择 char[] 数组而不是 String 作为密码。

1. 由于字符串在 Java 中是不可变的,如果您将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它,并且由于字符串在字符串池中用于可重用性,所以它很有可能会长时间保留在内存中,这构成了安全威胁。

由于任何有权访问内存转储的人都可以找到明文密码,这就是您应该始终使用加密密码而不是纯文本的另一个原因。由于字符串是不可变的,因此无法更改字符串的内容,因为任何更改都会产生新的字符串,而如果您使用 char[],您仍然可以将所有元素设置为空白或零。因此,将密码存储在字符数组中显然可以降低窃取密码的安全风险。

2. Java 本身建议使用 JPasswordField 的 getPassword() 方法,它返回一个 char[],而不是已弃用的 getText() 方法,它以明文形式返回说明安全原因的密码。遵循 Java 团队的建议并遵守标准而不是违背标准是很好的。

3. 使用 String 总是存在在日志文件或控制台中打印纯文本的风险,但是如果您使用数组,您将不会打印数组的内容,而是打印其内存位置。虽然不是真正的原因,但它仍然是有道理的。

String strPassword="Unknown";
char[] charPassword= new char[]{'U','n','k','w','o','n'};
System.out.println("String password: " + strPassword);
System.out.println("Character password: " + charPassword);

String password: Unknown
Character password: [C@110b053

引用自 this blog。我希望这有帮助。


这是多余的。这个答案是@SrujanKumarGulla stackoverflow.com/a/14060804/1793718 所写答案的精确版本。请不要复制粘贴或重复相同的答案两次。
1.) System.out.println("字符密码:" + charPassword); 有什么区别2.) System.out.println(charPassword);因为它给出了与输出相同的“未知”。
@Lucky不幸的是,您链接到的旧答案是从与此答案相同的博客中剽窃的,现在已被删除。请参阅meta.stackoverflow.com/questions/389144/…。这个答案只是从同一个博客中剪切和粘贴,没有添加任何内容,所以它应该只是一个链接到原始来源的评论。
“由于字符串在字符串池中用于可重用性,它很有可能会在内存中保留很长时间,这会带来安全威胁。”仅当您显式调用了 intern() 或者它是文字字符串时,字符串才会出现在字符串池中。无论如何,用户密码不能是文字字符串,并且不鼓励使用 intern() 方法,尽管字符串是密码。
C
Community

编辑: 在经过一年的安全研究后回到这个答案,我意识到这意味着您实际上会比较明文密码,这是相当不幸的暗示。请不要。 Use a secure one-way hash with a salt and a reasonable number of iterations。考虑使用库:这些东西很难做对!

原始答案: String.equals() 使用 short-circuit evaluation 并因此容易受到计时攻击这一事实又如何呢?这可能不太可能,但您可以理论上对密码比较进行计时,以确定正确的字符序列。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        // Quits here if Strings are different lengths.
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // Quits here at first different character.
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

有关定时攻击的更多资源:

定时攻击的教训

关于信息安全堆栈交换上的定时攻击的讨论

当然,Timing Attack Wikipedia 页面


但这也可能存在于 char[] 比较中,在某个地方我们也会在密码验证中做同样的事情。那么 char[] 怎么比字符串好呢?
你是绝对正确的,无论哪种方式都可能犯错误。了解问题是这里最重要的事情,考虑到 Java 中没有针对基于字符串或基于 char[] 的密码的显式密码比较方法。我想说对字符串使用 compare() 的诱惑是使用 char[] 的一个很好的理由。这样,您至少可以控制比较的完成方式(无需扩展 String,这在 imo 中很痛苦)。
除了比较明文密码无论如何都不是正确的事情之外,将 Arrays.equals 用于 char[] 的诱惑与 String.equals 一样高。如果有人关心的话,这里有一个专门的密钥类来封装实际密码并处理问题——哦等等,真正的安全包专门的密钥类,这个问答只是关于一个习惯 在它们之外,例如 JPasswordField,使用 char[] 而不是 String(实际算法无论如何都使用 byte[])。
安全相关软件在拒绝登录尝试之前应该执行类似 sleep(secureRandom.nextInt()) 的操作,这不仅消除了定时攻击的可能性,还可以抵消暴力尝试。
O
Oleg Mikheev

除非您在使用后手动清理它,否则 char 数组不会为您提供与 String 相比的任何东西,而且我还没有看到有人真正这样做。所以对我来说 char[] vs String 的偏好有点夸张。

看看广泛使用 Spring Security 库here,问问自己 - Spring Security 的人是无能的还是 char[] 密码没有多大意义。当一些讨厌的黑客窃取您 RAM 的内存转储时,即使您使用复杂的方法来隐藏它们,请确保他/她会获得所有密码。

然而,Java 一直在变化,一些可怕的特性(如 String Deduplication feature of Java 8)可能会在您不知情的情况下实习 String 对象。但这是一个不同的对话。


为什么字符串重复数据删除很可怕?它仅适用于至少有两个具有相同内容的字符串,那么让这两个已经相同的字符串共享同一个数组会产生什么危险呢?或者让我们反过来问:如果没有字符串重复数据删除,那么两个字符串都有一个不同的数组(具有相同的内容)这一事实会产生什么优势?在任何一种情况下,至少只要该内容中最长的存活字符串还活着,就会有一个该内容的数组是活着的……
@Holger 任何你无法控制的事情都是潜在的风险......例如,如果两个用户有相同的密码,这个奇妙的功能会将他们两个都存储在单个 char[] 中,从而很明显它们是相同的,不确定那是不是巨大的风险,但仍然
如果您可以访问堆内存和两个字符串实例,那么字符串是指向同一个数组还是指向两个具有相同内容的数组都没有关系,每个都很容易找到。特别是,因为无论如何它都无关紧要。如果你在这一点上,你抓住两个密码,无论是否相同。实际的错误在于使用明文密码而不是加盐哈希。
@Holger 验证密码它必须在内存中以明文形式存在一段时间,10 毫秒,即使它只是为了从中创建一个加盐哈希。然后,如果碰巧有两个相同的密码在内存中保存了 10 毫秒,那么重复数据删除可能会发挥作用。如果它真的实习字符串,它们会在内存中保存更长的时间。几个月不重启的系统会收集很多这些。只是理论。
看来,您对字符串重复数据删除存在根本性的误解。它不是“实习字符串”,它所做的只是让具有相同内容的字符串指向同一个数组,这实际上减少了包含明文密码的数组实例的数量,因为除了一个数组实例之外的所有数组实例都可以被回收和覆盖立即被其他物体。这些字符串仍然像任何其他字符串一样被收集。如果您了解重复数据删除实际上是由垃圾收集器完成的,那么它可能会有所帮助,对于仅在多个 GC 周期中幸存下来的字符串。
G
Geek

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

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


如果所讨论的 JVM > 1.6,它们只会被 GC 处理。在 1.7 之前,所有字符串都存储在 permgen 中。
@avgvstvs:“所有字符串都存储在 permgen 中”完全是错误的。只有实习字符串存储在那里,如果它们不是源自代码引用的字符串文字,它们仍然会被垃圾收集。考虑一下。如果字符串在 1.7 之前的 JVM 中通常不会被 GC,那么任何 Java 应用程序怎么能存活超过几分钟呢?
@Holger 这是错误的。实习 stringsString 池(以前使用的字符串池)都存储在 1.7 之前的 Permgen 中。此外,请参阅第 5.1 节:docs.oracle.com/javase/specs/jvms/se6/html/… JVM 总是检查 Strings 以查看它们是否是相同的参考值,并会为您调用 String.intern()。结果是每次 JVM 在 constant_pool 或堆中检测到相同的字符串时,都会将它们移动到 permgen。在 1.7 之前,我使用“creeping permgen”开发了几个应用程序。这是一个真正的问题。
回顾一下:在 1.7 之前,字符串从堆中开始,当它们被使用时,它们被放入位于 in permgen 的 constant_pool 中,然后如果一个字符串被多次使用,它将被拦截。
@avgvstvs:没有“以前使用的字符串池”。你把完全不同的东西放在一起。有一个运行时字符串池,其中包含字符串文字和显式内部字符串,但没有其他字符串。每个类都有其包含编译时常量的常量池。这些字符串会自动添加到运行时池中,但只有这些,而不是每个字符串。
A
ACV

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

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 崩溃并生成内存转储时 - 您将能够看到密码。

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


ch = null;你不能这样做
但是 request.getPassword() 不是已经创建了字符串并将其添加到池中吗?
ch = '0' 更改局部变量 ch;它对数组没有影响。无论如何,您的示例毫无意义,您从调用 toCharArray() 的字符串实例开始,创建一个新数组,即使您正确覆盖了新数组,它也不会更改字符串实例,因此它没有优势使用字符串实例。
@Holger 谢谢。更正了 char 数组清理代码。
您可以简单地使用 Arrays.fill(pass, '0');
t
tRuEsAtM

简短而直接的答案是因为 char[] 是可变的,而 String 对象不是。

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

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

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


@fallenidol 一点也不。仔细阅读,你会发现不同之处。
A
Aditya Rewari

案例字符串:

    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

S
Saathvik

java中的字符串是不可变的。因此,无论何时创建一个字符串,它都会保留在内存中,直到它被垃圾回收。所以任何有权访问内存的人都可以读取字符串的值。如果字符串的值被修改,那么它将最终创建一个新字符串。因此,原始值和修改后的值都保留在内存中,直到它被垃圾回收。使用字符数组,一旦达到密码的目的,就可以修改或删除数组的内容。数组的原始内容在修改后甚至在垃圾收集开始之前都不会在内存中找到。出于安全考虑,最好将密码存储为字符数组。


L
LJ Germain

是否应该为此使用 String 或使用 Char[] 是有争议的,因为两者都有其优点和缺点。这取决于用户需要什么。

由于 Java 中的字符串是不可变的,因此每当有人尝试操作您的字符串时,它都会创建一个新对象,而现有的字符串不受影响。这可以被视为将密码存储为字符串的优势,但即使在使用后该对象仍保留在内存中。因此,如果有人以某种方式获得了对象的内存位置,则该人可以轻松追踪存储在该位置的密码。

Char[] 是可变的,但它的优点是在使用后程序员可以显式清理数组或覆盖值。因此,当它使用完毕时,它会被清理干净,没有人会知道您存储的信息。

根据以上情况,可以根据自己的需求判断是使用 String 还是使用 Char[] 。


D
Dibsyhex

上面有很多很棒的答案。我假设还有一点(如果我错了,请纠正我)。默认情况下,Java 使用 UTF-16 存储字符串。使用字符数组 char[]array 有助于使用 unicode、区域字符等。这种技术允许在存储密码时平等地尊重所有字符集,并且今后不会由于字符集混淆而引发某些加密问题。最后使用 char 数组,我们可以将密码数组转换为我们想要的字符集字符串。


Java 中的 char 是 16 位值,并且 char 数组也被假定为 UTF-16 编码。 docs.oracle.com/javase/7/docs/api/java/lang/… 如果您打算以不同的编码存储字符串,我认为您必须改用字节数组。要获得不同编码的字符串,可以使用 String.getBytes。