Spring客户端连接SSH键盘交互式服务器时提示访问方法不支持
问题:Spring服务连接键盘交互式SSH服务器返回“access method not supported”
我正在开发一个Spring服务,需要连接支持键盘交互式认证的SSH服务器。目前已成功建立会话和通道,但通过InputStream收到服务器返回的“access method not supported”响应,这并非异常,而是服务器的正常返回内容。
用PuTTY连接该服务器时,能够正常执行命令并获取响应。通过Wireshark抓包对比发现:
- PuTTY连接时产生58个加密SSH数据包
- Spring服务连接时仅产生28个加密SSH数据包
当前代码实现
UserAuthKI组件
@Component public class UserAuthKI { @SneakyThrows public String connectAndExecuteCommand(String username, String password) { Session session = createSession(5, username, password); try { return createChannelExec("display-alarms", session); } catch (Exception e) { return "unsuccessful connection to the ssh server"; } } private Session createSession(int retries, String username, String password) { if (retries == 0) { throw new RuntimeException("out of tries, unable to connect to session"); } try { String host = "xxx.xx.xxx.xxx"; Session session = new JSch().getSession(username, host, 22); configureAndConnectSession(session, password); return session; } catch (Exception e) { return createSession(retries - 1, username, password); } } @SneakyThrows private void configureAndConnectSession(Session session, String password) { UserInfo info = new MyUserInfo(password); session.setUserInfo(info); java.util.Properties config = new java.util.Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); session.connect(); } @NotNull @SneakyThrows private String createChannelExec(String command, Session session) { StringBuilder sb = new StringBuilder(); ChannelExec channelExec = (ChannelExec) session.openChannel("exec"); channelExec.setCommand(command); Scanner sc = new Scanner(channelExec.getInputStream()); channelExec.connect(); while (sc.hasNext()) { sb.append(sc.next()); } sc.close(); closeSessionAndChannel(session, channelExec); System.out.println(sb.toString()); return sb.toString(); } private void closeSessionAndChannel(Session session, ChannelExec channel) { channel.disconnect(); session.disconnect(); } }
键盘交互认证实现MyUserInfo
public class MyUserInfo implements UserInfo, UIKeyboardInteractive { private String passwd; public MyUserInfo(String password) { this.passwd = password; } @Override public String getPassword() { return passwd; } @Override public boolean promptYesNo(String str) { return false; } @Override public String getPassphrase() { return null; } @Override public boolean promptPassphrase(String message) { return false; } @Override public boolean promptPassword(String message) { return true; } @Override public void showMessage(String message) { } @Override public String[] promptKeyboardInteractive(String destination, String name, String instruction,String[] prompt, boolean[] echo) { String[] response=new String[prompt.length]; response[0] = passwd; return response; } }
问题分析
- 认证方式优先级问题:JSch默认优先尝试密码认证,而非键盘交互式认证,即使实现了
UIKeyboardInteractive接口,服务器可能要求必须通过键盘交互式认证才能执行命令,导致认证流程不符合要求。 - 键盘交互实现不完善:
promptYesNo返回false可能拒绝服务器的某些确认请求,部分场景下会中断认证流程;promptKeyboardInteractive仅处理第一个提示,若服务器有多轮交互式提问会无法正确响应。 - 通道流处理顺序错误:先获取输入流再连接通道,可能导致无法完整捕获服务器响应;未处理错误流,可能导致通道因未读取错误输出而阻塞。
- 无连接超时:网络延迟可能导致认证流程不完整,数据包数量远少于PuTTY的原因是会话未完成完整的认证交互。
解决方案
1. 强制优先使用键盘交互式认证
在configureAndConnectSession方法中添加认证顺序配置:
@SneakyThrows private void configureAndConnectSession(Session session, String password) { UserInfo info = new MyUserInfo(password); session.setUserInfo(info); java.util.Properties config = new java.util.Properties(); config.put("StrictHostKeyChecking", "no"); // 优先使用键盘交互式认证,再尝试密码认证 config.put("PreferredAuthentications", "keyboard-interactive,password"); session.setConfig(config); // 设置连接超时,避免无限等待 session.connect(30000); }
2. 完善键盘交互实现
调整promptYesNo方法,并确保处理所有交互式提示:
public class MyUserInfo implements UserInfo, UIKeyboardInteractive { private String passwd; public MyUserInfo(String password) { this.passwd = password; } @Override public String getPassword() { return passwd; } @Override public boolean promptYesNo(String str) { // 允许服务器的确认请求,比如主机密钥相关提示(根据实际场景调整) return true; } @Override public String getPassphrase() { return null; } @Override public boolean promptPassphrase(String message) { return false; } @Override public boolean promptPassword(String message) { return true; } @Override public void showMessage(String message) { // 打印服务器提示信息,便于调试 System.out.println("Server message: " + message); } @Override public String[] promptKeyboardInteractive(String destination, String name, String instruction,String[] prompt, boolean[] echo) { String[] response = new String[prompt.length]; // 处理所有提示,若有多个提问,根据实际情况填充响应 for (int i = 0; i < prompt.length; i++) { // 假设所有提示都使用密码响应,可根据实际提示内容调整 response[i] = passwd; } return response; } }
3. 修正通道流处理逻辑
调整通道连接和流读取顺序,同时处理错误流:
@NotNull @SneakyThrows private String createChannelExec(String command, Session session) { StringBuilder sb = new StringBuilder(); ChannelExec channelExec = (ChannelExec) session.openChannel("exec"); channelExec.setCommand(command); // 先连接通道 channelExec.connect(10000); // 处理标准输出流 Scanner sc = new Scanner(channelExec.getInputStream(), "UTF-8"); while (sc.hasNextLine()) { sb.append(sc.nextLine()).append("\n"); } // 处理错误输出流,避免通道阻塞 Scanner errorSc = new Scanner(channelExec.getErrStream(), "UTF-8"); while (errorSc.hasNextLine()) { sb.append("Error: ").append(errorSc.nextLine()).append("\n"); } sc.close(); errorSc.close(); // 等待通道完成 while (!channelExec.isClosed()) { Thread.sleep(100); } closeSessionAndChannel(session, channelExec); String result = sb.toString().trim(); System.out.println(result); return result; }
内容的提问来源于stack exchange,提问作者codemaster1101
相关产品推荐
相关产品推荐

