bigywater.tech
Java · 405

Console:交互式终端中的安全输入

Console 是 Java 里唯一内置的“不回显”输入方式,但它只在真正的终端里存在。这篇用伪终端和管道对比运行,量出它在三个 JDK 版本里什么时候是 null、密码是否真的不回显、EOF 的行为,以及字符集的坑。

null
stdin / stdout 是管道时 System.console() 的值(JDK 17、21、27 都一样)
False vs True
readPassword 与 Scanner 读入的密码出现在终端输出里的情况
US-ASCII vs UTF-8
LC_ALL=C 时 Console.charset() 与 Charset.defaultCharset()(JDK 21)

什么时候有 Console

实测 · 本机执行输出
  [JDK 17] 管道:System.console() == null;System.in.available() 可读 = true;jdk.console = null
  [JDK 17] 伪终端:System.console() != null(java.io.Console);(无 isTerminal 方法);System.in.available() 可读 = true;jdk.console = null
  [JDK 21] 管道:System.console() == null;System.in.available() 可读 = true;jdk.console = null
  [JDK 21] 伪终端:System.console() != null(java.io.ProxyingConsole);(无 isTerminal 方法);System.in.available() 可读 = true;jdk.console = null
  [JDK 27] 管道:System.console() == null;System.in.available() 可读 = true;jdk.console = null
  [JDK 27] 伪终端:System.console() != null(java.io.ProxyingConsole);isTerminal() = true;System.in.available() 可读 = true;jdk.console = null
  [JDK 27] 管道 + -Djdk.console=java.base:System.console() == null;System.in.available() 可读 = true;jdk.console = java.base

回显

实测 · 本机执行输出
  [JDK 17] readLine + readPassword 的终端内容:姓名: <<发送>>alice⏎密码: <<发送>>⏎RES 读到姓名 alice,密码长度 10,密码首字符 S⏎RES 用完后 Arrays.fill 清零,pw[0] = 0⏎
      密码 Secret123! 出现在终端输出里 = False;姓名 alice 的回显在终端里 = True
  [JDK 17] Scanner:密码(用 Scanner 读): <<发送>>Secret123!⏎RES Scanner 读到长度 10⏎  → 密码 Secret123! 出现在终端输出里 = True(终端回显)
  [JDK 21] readLine + readPassword 的终端内容:姓名: <<发送>>alice⏎密码: <<发送>>⏎RES 读到姓名 alice,密码长度 10,密码首字符 S⏎RES 用完后 Arrays.fill 清零,pw[0] = 0⏎
      密码 Secret123! 出现在终端输出里 = False;姓名 alice 的回显在终端里 = True
  [JDK 21] Scanner:密码(用 Scanner 读): <<发送>>Secret123!⏎RES Scanner 读到长度 10⏎  → 密码 Secret123! 出现在终端输出里 = True(终端回显)
  [JDK 27] readLine + readPassword 的终端内容:姓名: <<发送>>alice⏎密码: <<发送>>⏎RES 读到姓名 alice,密码长度 10,密码首字符 S⏎RES 用完后 Arrays.fill 清零,pw[0] = 0⏎
      密码 Secret123! 出现在终端输出里 = False;姓名 alice 的回显在终端里 = True
  [JDK 27] Scanner:密码(用 Scanner 读): <<发送>>Secret123!⏎RES Scanner 读到长度 10⏎  → 密码 Secret123! 出现在终端输出里 = True(终端回显)

EOF 与字符集

实测 · 本机执行输出
  [JDK 21] Ctrl-D:密码: <<发送>>⏎RES 输入 Ctrl-D 之后 readPassword 返回 null⏎再读一行: <<发送>>^DRES 之后 readLine 返回 null⏎
  [JDK 27] Ctrl-D:密码: <<发送>>⏎RES 输入 Ctrl-D 之后 readPassword 返回 null⏎再读一行: <<发送>>^DRES 之后 readLine 返回 null⏎
  [JDK 17] 伪终端,默认环境:Console.charset() = UTF-8;Charset.defaultCharset() = UTF-8;stdout.encoding = null;native.encoding = UTF-8
  [JDK 21] 伪终端,默认环境:Console.charset() = UTF-8;Charset.defaultCharset() = UTF-8;stdout.encoding = UTF-8;native.encoding = UTF-8
  [JDK 27] 伪终端,默认环境:Console.charset() = UTF-8;Charset.defaultCharset() = UTF-8;stdout.encoding = UTF-8;native.encoding = UTF-8
  [JDK 21] 伪终端,LC_ALL=C:Console.charset() = US-ASCII;Charset.defaultCharset() = UTF-8;stdout.encoding = US-ASCII;native.encoding = US-ASCII
  [JDK 21] 管道:Console.charset() = (无 charset 方法);Charset.defaultCharset() = UTF-8;stdout.encoding = UTF-8;native.encoding = UTF-8

分步演示

Console
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 17.0.20、21.0.12、27;伪终端由 Python 的 pty 模块提供(模拟终端,不是真实终端模拟器);输入由脚本在看到提示后发送
    • 没有验证:Windows 控制台(cmd / PowerShell)、IDE 内置终端(通常没有 tty)、Jline 提供的 jdk.console=jdk.internal.le 实现(JDK 22+ 的新默认值在 JDK 27 上没有观察到管道里非 null,原因未查)、ssh 与 tmux 下的行为、Unicode / 组合键输入
    京ICP备2026067502号-1京公网安备11010502063487号