IMLC.ME

Apache MINA SSHD 在密码过期后如何修改密码

在手动 SSH 远端服务器时,如果遇到密码过期,SSH 会要求你更新密码。

一般来说,更新密码有三步:

  1. 输入当前密码
  2. 输入新密码
  3. 再次输入新密码

我遇到的问题是,MINA SSHD 能正常输入当前密码和新密码,但是一直无法到达 "再次输入密码" 这一步。 由于我对 SSH 并不熟悉,而目标服务器也是自定义的 SSH 服务端(不是日常的 SSH 到服务器,而是通过 SSH 协议提供交互服务), 我不是很清楚这个问题到底是 MINA SSHD 的 BUG,又或者是 SSH 服务端没有标准地实现 SSH 服务。 总之,在默认的配置下,MINA SSHD 走不到再次输入密码这一步。

问题现象

使用 MINA SSHD 走 keyboard-interactive 方式做登录验证。 在 UserInteraction 的 interactive() 方法中 ,依次收到了输入当前密码和输入新密码的消息。 而且程序到此就没有下文,无法接受到第三步——再次输入密码,因而也无法成功更新密码。

问题原因

开启 DEBUG 日志追踪程序行为,结合源代码,发现 MINA SSHD 与服务器的每次交互受 maxTrials 限制,其默认值为 3. MINA SSHD 与服务器的交互为: 0. 尝试用密码方式登录

  1. 尝试用 Public Key 方式登录
  2. 尝试以 Keyboard Interactive(键盘交互)方式登录 - 接收到密码过期,输入当前密码的消息
  3. 继续以 Keyboard Interactive(键盘交互)方式登录 - 接收到输入新密码的消息
  4. 发现当前交互次数 4 > 最大交互次数 maxTrials(3), 不再响应新消息

这里看着很像是个 bug。 实现该逻辑的代码位于 UserAuthKeyboardInteractive.verifyTrialsCount()。 该方法从0开始计数,最后判断 nbTrials <= maxAllowed见代码)。换言之,实际的交互次数最大值为4...

算了,不纠结。总之,解决方法显而易见,我要调高 maxAllowed。 继续追踪代码,不难看到,maxAllowed 的值来自于

maxTrials = CoreModuleProperties.PASSWORD_PROMPTS.getRequired(session);

问题就是如何修改 CoreModuleProperties.PASSWORD_PROMPTS 的值。

解决办法

这鬼东西在网上也没什么资料,逼不得已继续追溯代码。但不得不说这个 CoreModuleProperties 的使用方法真的不太好猜。看了半天完全走错了思路。总之,show you the code。

public class Main {

  public static void main(String[] args) throws IOException {
    SshClient client = SshClient.setUpDefaultClient();

    String currentPassword = "currentPassword123";
    String newPassword = "newPassword123";
    
    // 通过实现 UserInteraction 接口,完成 keyboard interactive 方式的登录验证
    client.setUserInteraction(new UserInteraction() {
      @Override
      public String[] interactive(ClientSession session, String name, String instruction,
          String lang, String[] prompt, boolean[] echo) {
        if(prompt.length > 0) {
          
          // prompt 数组里记录了服务端的消息,在我的情景中,只需要根据 prompt[0] 中消息决定如何响应
          String msg = prompt[0];

          if(msg.contains("Current password")) {
            return new String[]{currentPassword};
          }

          if(msg.contains("New password") || msg.contains("Reenter password")) {
            return new String[]{newPassword};
          }
        }

        throw new UnsupportedOperationException(
            "Unsupported interactive for prompt: {}" +
                String.join(",", prompt)
        );
      }

      @Override
      public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
        return null;
      }
    });

    client.start();

    ConnectFuture connectFuture = client.connect("username", "example.com", 22);
    try (ClientSession session = connectFuture.verify(30, TimeUnit.SECONDS)
        .getSession()) {
      
      // 调高 PASSWORD_PROMPTS 以修复“再次输入新密码”无法响应的问题
      CoreModuleProperties.PASSWORD_PROMPTS.set(session, 4);

      session.auth().verify(30, TimeUnit.SECONDS);

      ChannelShell shellChannel = session.createShellChannel();
      // ...
    }

  }

}