Apache MINA SSHD 在密码过期后如何修改密码
在手动 SSH 远端服务器时,如果遇到密码过期,SSH 会要求你更新密码。
一般来说,更新密码有三步:
- 输入当前密码
- 输入新密码
- 再次输入新密码
我遇到的问题是,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. 尝试用密码方式登录
- 尝试用 Public Key 方式登录
- 尝试以 Keyboard Interactive(键盘交互)方式登录 - 接收到密码过期,输入当前密码的消息
- 继续以 Keyboard Interactive(键盘交互)方式登录 - 接收到输入新密码的消息
- 发现当前交互次数 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();
// ...
}
}
}