1. 概述

在Java开发中,关于密码的内存操作有一个经典建议:**优先使用 char[] 数组来表示密码,而不是 String**。

⚠️ 需要强调的是,本文讨论的是密码在内存中的操作与生命周期管理,而非持久化存储(如数据库加密、哈希存储等),后者属于安全存储范畴,通常由持久层或安全框架处理。

即使我们无法控制密码的输入格式(比如第三方API传来的密码是 String 类型),也应尽快将其转换为 char[] 进行后续处理。这并非空穴来风——Java官方团队本身也推荐这种做法

一个典型的例子是 javax.swing.JPasswordField 类:

  • getText() 方法(返回 String)自 Java 2 起就被标记为 @Deprecated
  • 取而代之的是 getPassword() 方法,它返回 char[]

✅ 所以,这不是“个人偏好”,而是有明确设计依据的最佳实践。下面我们来看几个关键原因。


2. String 的不可变性带来安全隐患

String 在 Java 中是不可变的(immutable),这意味着:

  • 任何对 String 的“修改”都会创建一个新的 String 实例
  • 原始字符串对象仍保留在内存中,直到垃圾回收(GC)发生
  • String 通常会被放入 字符串常量池(String Pool)以支持复用,导致其生命周期更长

这就带来一个严重问题:密码明文可能长时间驻留在内存中,一旦系统内存被 dump,攻击者就能从中提取出原始密码。

相比之下,char[] 是可变的(mutable),我们可以主动清空其内容:

✅ 使用完后立即擦除数据,避免敏感信息残留

示例对比

使用 String(错误做法)

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

❌ 可以看到,replace() 并没有真正修改原字符串,旧的 "password" 依然留在内存中。

使用 char[](推荐做法)

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: 7cc355be
Changed char password value: ********
Changed char password hashCode: 7cc355be

✅ 虽然 hashCode 没变(因为是同一个对象),但我们成功修改了数组内容。更重要的是:

用完后可以调用 Arrays.fill(password, '\0') 彻底清空,确保密码不会残留在内存堆中。

这是 String 完全做不到的。


3. 防止密码被意外打印

另一个常被忽视的风险是:**String 容易被无意中输出到日志、控制台或监控系统**。

看下面这段代码:

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 -> [C@6e8cf4c6

⚠️ 第二个输出是 char[] 的默认 toString() 结果(类名 + 哈希码),不会暴露实际内容

这意味着:

  • 即使你在调试时误打了 char[],也不会泄露密码
  • String 一旦被拼接到日志中,明文就直接暴露了

✅ 这种“天然防呆”机制让 char[] 在安全性上更胜一筹。


4. 总结

对比项 String char[]
可变性 ❌ 不可变,无法主动清除 ✅ 可变,可用 Arrays.fill() 擦除
内存残留风险 ⚠️ 高(常量池 + GC 不可控) ✅ 低(可手动清空)
日志泄露风险 ⚠️ 高(toString 输出明文) ✅ 低(输出为地址)
官方推荐 ❌ 不推荐用于密码 ✅ 推荐(如 JPasswordField)

📌 核心结论:

处理密码时,能用 char[] 就别用 String。这不是过度设计,而是最小化攻击面的简单粗暴手段。

虽然现代JVM和安全框架已经做了很多防护,但在涉及敏感数据时,多一层主动控制,就少一分被动风险


示例代码已托管至 GitHub:https://github.com/example-java/security-password-handling


原始标题:Use char[] Array Over a String for Manipulating Passwords in Java?