这是一个非常好的安全实践问题。简单来说,char[] 可以及时清除敏感数据,而 String 会将其长期保留在内存中,增加泄露风险。

下面我从技术原理、安全场景和最佳实践三个层面详细解释:


一、核心安全差异

1. 不可变性的双刃剑
  • String 的问题:一旦创建就无法被修改或清空。即使你将引用设为 null,实际数据仍在内存中,直到被GC回收(时间不确定)。

  • char[] 的优势:使用后可以立即用空白或随机数据覆盖:

    java

    char[] password = getUserInput();
    // 使用密码进行验证...
    Arrays.fill(password, '0'); // 立即清除内存中的密码
2. 内存驻留风险

String 的特殊存储机制

java

String password = "mySecret123";
  • 可能进入字符串常量池(如果通过字面量创建)

  • 即使不被池化,也会在堆内存中长期保留(直到GC)

  • 可能被内存转储(Heap Dump)或调试工具捕获

真实案例:通过 jmap -dump:live,file=heap.bin <pid> 获取堆转储后,用MAT等工具可以搜索到明文字符串密码。

3. 控制台输入的安全处理

观察 Console 类的设计:

java

public final class Console {
    public char[] readPassword(String fmt, Object... args) {
        // 返回 char[] 而不是 String!
        return readline(false, fmt, args);
    }
}

Java标准库已经做了最佳实践示范。


二、攻击场景分析

1. 内存扫描攻击

攻击者可以通过读取进程内存来获取敏感信息:

  • String:在内存中连续存储,易于识别和提取

  • char[]:及时清除后,攻击窗口极小

2. 日志意外记录

java

// 危险!密码可能被记录到日志文件
String password = request.getParameter("password");
logger.info("User login with password: " + password); // 开发人员可能无意中这样写

// 更安全的做法
char[] password = request.getParameter("password").toCharArray();
// 即使被日志记录,也无法直接看到内容
3. 线程安全问题

在多线程环境中:

  • String 可能被多个线程共享(如作为参数传递)

  • char[] 可以更好地控制生命周期和访问范围

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】​​​


三、实际代码对比

不安全示例(使用String)

java

public class AuthenticationService {
    private String cachedPassword; // 危险!
    
    public boolean login(String input) {
        // password 在内存中长期存在
        String password = loadPasswordFromDB(); 
        
        boolean result = password.equals(input);
        password = null; // 这只是将引用置空,数据还在内存中!
        return result;
    }
}
安全示例(使用char[])

java

public class SecureAuthenticationService {
    public boolean login(char[] input) {
        char[] storedPassword = null;
        try {
            storedPassword = loadPasswordFromDB(); // 返回char[]
            
            // 安全的比较,避免时序攻击
            boolean result = slowEquals(storedPassword, input);
            
            // 重要:使用后立即清除
            Arrays.fill(storedPassword, '0');
            Arrays.fill(input, '0');
            
            return result;
        } finally {
            // 确保无论如何都清除
            if (storedPassword != null) {
                Arrays.fill(storedPassword, '0');
            }
        }
    }
    
    // 防止时序攻击的比较方法
    private boolean slowEquals(char[] a, char[] b) {
        int diff = a.length ^ b.length;
        for (int i = 0; i < a.length && i < b.length; i++) {
            diff |= a[i] ^ b[i];
        }
        return diff == 0;
    }
}

四、延伸的安全考虑

1. 不仅仅是密码

同样适用于:

  • 加密密钥(encryption keys)

  • API密钥(API tokens)

  • 信用卡号

  • 其他敏感凭证

2. 现代框架的实践
  • Spring SecurityPasswordEncoder 接口处理 CharSequence

  • OWASP建议:明确推荐使用 char[] 存储密码

  • 密码管理器:都使用安全内存区域存储敏感数据

3. JVM层面的缓解(较新特性)

java

// Java 8+ 可以使用Direct ByteBuffer
java.nio.ByteBuffer.allocateDirect(size);

// 或者使用安全管理器控制访问
System.setSecurityManager(new SecurityManager());

五、面试回答要点

在面试中回答这个问题时,建议按以下结构:

  1. 核心原因char[] 可及时清除内存,String 因不可变性长期驻留

  2. 安全风险

    • 内存转储泄露

    • 日志意外记录

    • 线程间传播

  3. 设计佐证Console.readPassword() 返回 char[]

  4. 最佳实践

    • 使用后立即用 Arrays.fill() 清除

    • 使用常量时间比较防止时序攻击

    • 在finally块中确保清除

  5. 延伸思考:不仅密码,所有敏感数据都应考虑内存安全

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】​​​


重要提醒

虽然 char[] 比 String 更安全,但这只是纵深防御的一层。完整的安全方案还需要:

  • 传输层加密(HTTPS/TLS)

  • 密码哈希加盐存储(如bcrypt)

  • 适当的访问控制

  • 安全的密码策略

这种设计体现了安全领域的一个重要原则:尽量减少敏感数据在内存中的停留时间和暴露范围

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐