Apache SSHD 2.20 嵌入式集成使用指南
Apache SSHD 2.20 从 0 到 1 嵌入 Java 应用:最小依赖准备、嵌入式 SSH 服务端搭建、shell/exec/SFTP 三通道实测、SSHD 客户端连接 OpenSSH 实测,以及 CommandFactory 配错导致 exec 无输出的排查实录
Apache SSHD 2.20 嵌入式集成使用指南
目标:在一台 Ubuntu 22.04 服务器上,从 0 到 1 把 Apache SSHD 2.20.0 嵌入一个 Java 进程,对外提供 SSH 登录(shell)、远程命令(exec)、SFTP 文件传输三类能力,并演示用 SSHD 客户端 API 连接标准 OpenSSH 服务端。本文所有命令与输出均实际执行验证。
一、环境与目标
实测环境:
- 操作系统:Ubuntu 22.04.5 LTS(x86_64),
root密码q1w2e3r4 - Java:OpenJDK 17.0.20.1(
javac17.0.20.1 编译) - SSHD:Apache SSHD 2.20.0(sshd-core / sshd-common / sshd-sftp)
- 对照组件:OpenSSH_8.9p1 Ubuntu-3ubuntu0.18(系统自带 sshd 监听 2022 端口、
ssh/sftp/sshpass客户端) - 应用登录账号:
admin/q1w2e3r4(由 SSHD 认证回调校验,与 OS 账号无关)
完成后你将得到:
- 一个监听 2222 端口的嵌入式 SSH 服务端(Java 进程内);
- 系统
ssh可登录执行命令、交互 shell; - 系统
sftp可上传下载文件; - 一个 SSHD 客户端示例,连接 OpenSSH 服务端执行远程命令。
$ java -version
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
二、准备最小依赖
SSH 服务端(不含 SFTP)只需 4 个 jar;SFTP 追加 sshd-sftp。建立工作目录 /opt/sshd,从 Maven Central 直接下载:
root@server:~# mkdir -p /opt/sshd/lib /opt/sshd/classes && cd /opt/sshd/lib
root@server:~# M=https://repo1.maven.org/maven2
root@server:~# curl -sSLO $M/org/apache/sshd/sshd-core/2.20.0/sshd-core-2.20.0.jar
root@server:~# curl -sSLO $M/org/apache/sshd/sshd-common/2.20.0/sshd-common-2.20.0.jar
root@server:~# curl -sSLO $M/org/apache/sshd/sshd-sftp/2.20.0/sshd-sftp-2.20.0.jar
root@server:~# curl -sSLO $M/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
root@server:~# curl -sSLO $M/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar
实测下载结果:
-rw-r--r-- 1 root root 41125 slf4j-api-1.7.36.jar
-rw-r--r-- 1 root root 15333 slf4j-simple-1.7.36.jar
-rw-r--r-- 1 root root 966659 sshd-common-2.20.0.jar
-rw-r--r-- 1 root root 970864 sshd-core-2.20.0.jar
-rw-r--r-- 1 root root 394207 sshd-sftp-2.20.0.jar
说明:
- 客户端类(SshClient 等)就在 sshd-core 里,没有独立的 sshd-client 制品(实测访问
.../sshd-client/2.20.0/sshd-client-2.20.0.jar返回 404),不要白找依赖; - SSHD 只强依赖 slf4j-api,用 slf4j-simple 即可直接运行。
三、最小嵌入式 SSH 服务端
新建 /opt/sshd/EmbeddedSshd.java(此版本先不含 SFTP,SFTP 在第 6 节追加):
import java.nio.file.Paths;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
import org.apache.sshd.server.shell.ProcessShellCommandFactory;
public class EmbeddedSshd {
public static void main(String[] args) throws Exception {
SshServer sshd = SshServer.setUpDefaultServer();
sshd.setHost("0.0.0.0");
sshd.setPort(2222);
// 1) 主机密钥:首次启动自动生成并持久化到 hostkey.ser
SimpleGeneratorHostKeyProvider keyProvider = new SimpleGeneratorHostKeyProvider();
keyProvider.setPath(Paths.get("/opt/sshd/hostkey.ser"));
keyProvider.setAlgorithm("RSA");
keyProvider.setKeySize(2048);
sshd.setKeyPairProvider(keyProvider);
// 2) 密码认证:回调里自定义校验逻辑(这里固定 admin/q1w2e3r4)
sshd.setPasswordAuthenticator((username, password, session) ->
"admin".equals(username) && "q1w2e3r4".equals(password));
// 3) 三类通道三个工厂:
// shell 通道(ssh 不带命令) -> ShellFactory
// exec 通道(ssh host cmd) -> CommandFactory
sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
sshd.setCommandFactory(ProcessShellCommandFactory.INSTANCE);
sshd.start();
System.out.println("Embedded SSHD listening on 0.0.0.0:2222, version=" + sshd.getVersion());
Thread.sleep(Long.MAX_VALUE); // 保持进程存活
}
}
三个工厂的分工(SSHD 服务端的核心概念):
| 客户端行为 | SSH 通道 | 服务端需设置 | 本文采用 |
|---|---|---|---|
ssh host(交互登录) | shell | setShellFactory(...) | InteractiveProcessShellFactory.INSTANCE |
ssh host "cmd"(远程命令) | exec | setCommandFactory(...) | ProcessShellCommandFactory.INSTANCE |
sftp/scp 文件传输 | subsystem | setSubsystemFactories(...) | 第 6 节追加 SftpSubsystemFactory |
四、编译与启动
root@server:~# cd /opt/sshd
root@server:~# javac -cp "lib/*" -d classes EmbeddedSshd.java && echo COMPILE_OK
COMPILE_OK
root@server:~# nohup java -cp "lib/*:classes" EmbeddedSshd > server.log 2>&1 &
启动日志(server.log):
[main] INFO org.apache.sshd.common.io.DefaultIoServiceFactoryFactory - No detected/configured IoServiceFactoryFactory; using Nio2ServiceFactoryFactory
Embedded SSHD listening on 0.0.0.0:2222, version=APACHE-SSHD-2.20.0
首次启动后生成主机密钥文件:
$ ls -l /opt/sshd/hostkey.ser
-rw------- 1 root root 1811 hostkey.ser
五、实测:系统 ssh 客户端连接
5.1 exec 通道(远程命令)
$ sshpass -p q1w2e3r4 ssh -o StrictHostKeyChecking=no -p 2222 admin@127.0.0.1 "whoami && hostname && uname -r && echo EMBEDDED_SSH_OK"
root
27382ed72b0f
6.8.0-138-generic
EMBEDDED_SSH_OK
服务端日志同步记录认证成功:
[sshd-SshServer[1442d7b5](port=2222)-nio2-thread-3] INFO org.apache.sshd.server.session.ServerSessionImpl - Session admin@/127.0.0.1:35418 authenticated
注意
whoami返回root:登录账号是 SSHD 自定义的 admin,但命令以 SSHD 进程的 OS 用户 身份执行(SSHD 不做 OS 用户映射,官方文档称之为无权限分离)。生产环境务必用非特权用户运行服务进程。
5.2 shell 通道(交互 shell)
$ printf "echo SHELL_OK\nwhoami\npwd\nexit\n" | sshpass -p q1w2e3r4 ssh -p 2222 admin@127.0.0.1
SHELL_OK
root
/opt/sshd
shell 的工作目录是服务进程的工作目录(本例为 /opt/sshd),同样印证"继承服务进程上下文"。
5.3 错误密码被拒绝
$ sshpass -p wrongpass ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o NumberOfPasswordPrompts=1 -p 2222 admin@127.0.0.1 whoami
Password authentication
$ echo $?
5
sshpass 退出码 5 表示认证被拒;服务端日志中也没有该连接的 authenticated 记录。
六、实测:SFTP 文件传输
6.1 追加 SFTP 子系统
sshd-core 不含 SFTP 实现,需要 sshd-sftp(已在第 2 节下载)。在 EmbeddedSshd.java 中追加两处:
import java.util.Collections;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
重新编译并重启服务端(先结束旧进程再启动):
root@server:~# javac -cp "lib/*" -d classes EmbeddedSshd.java && echo COMPILE_OK
COMPILE_OK
6.2 系统 sftp 命令上传/下载
$ printf "cd /tmp\nput /etc/hostname upload_from_sftp.txt\nls -l upload_from_sftp.txt\nget upload_from_sftp.txt roundtrip.txt\nbye\n" | sshpass -p q1w2e3r4 sftp -o StrictHostKeyChecking=no -P 2222 admin@127.0.0.1
Connected to 127.0.0.1.
sftp> cd /tmp
sftp> put /etc/hostname upload_from_sftp.txt
Uploading /etc/hostname to /tmp/upload_from_sftp.txt
sftp> ls -l upload_from_sftp.txt
-rw-r--r-- ? 0 0 13 Oct 15 03:57 upload_from_sftp.txt
sftp> get upload_from_sftp.txt roundtrip.txt
Fetching /tmp/upload_from_sftp.txt to roundtrip.txt
sftp> bye
三个文件(源文件、服务端落盘文件、客户端取回文件)md5 完全一致,SFTP 往返闭环:
$ md5sum /etc/hostname /tmp/upload_from_sftp.txt /roundtrip.txt
534565784c64f0f0b1576e422a485cdc /etc/hostname
534565784c64f0f0b1576e422a485cdc /tmp/upload_from_sftp.txt
534565784c64f0f0b1576e422a485cdc /roundtrip.txt
ls -l显示属主0 0(uid/gid 为 0)且中间一个字段为?:这是 SFTP 协议 longname 由服务端SftpSubsystem生成时的固有形态(SSHD 服务进程以 root 运行),属正常现象,不影响读写。
七、实测:SSHD 作为客户端连接 OpenSSH
SSHD 也能当 SSH 客户端用。新建 /opt/sshd/SshdClientDemo.java,连接本机 OpenSSH 服务端(监听 2022,OS 账号 admin/q1w2e3r4):
import java.util.EnumSet;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.session.ClientSession;
public class SshdClientDemo {
public static void main(String[] args) throws Exception {
SshClient client = SshClient.setUpDefaultClient();
client.start();
try (ClientSession session = client.connect("admin", "127.0.0.1", 2022)
.verify(10000).getSession()) {
session.addPasswordIdentity("q1w2e3r4");
session.auth().verify(10000);
System.out.println("Authed as " + session.getUsername()
+ ", server version=" + session.getServerVersion());
try (ClientChannel channel = session.createExecChannel("uname -a && uptime && id")) {
channel.setOut(System.out);
channel.setErr(System.err);
channel.open().verify(10000);
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 30000);
}
} finally {
client.stop();
}
}
}
编译运行:
root@server:~# javac -cp "lib/*" -d classes SshdClientDemo.java
root@server:~# java -cp "lib/*:classes" SshdClientDemo
实测输出:
[main] INFO org.apache.sshd.common.io.DefaultIoServiceFactoryFactory - No detected/configured IoServiceFactoryFactory; using Nio2ServiceFactoryFactory
[sshd-SshClient[4d1b0d2a]-nio2-thread-4] WARN org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier - Server at /127.0.0.1:2022 presented unverified EC key: SHA256:FU32KNhnVoAfDkGnBjacGiljDn0S0Q2ERPRVVMT6Ni8
Authed as admin, server version=SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.18
Linux 27382ed72b0f 6.8.0-138-generic #138~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug 7 13:43:15 UTC x86_64 x86_64 x86_64 GNU/Linux
03:56:44 up 3:48, 0 users, load average: 0.83, 0.72, 0.79
uid=1000(admin) gid=1000(admin) groups=1000(admin)
三个关键点:
Authed as admin, server version=SSH-2.0-OpenSSH_8.9p1:认证成功且取到对端版本串;- WARN
AcceptAllServerKeyVerifier ... unverified EC key:演示代码用了"不校验主机指纹"的默认 verifier,生产必须替换为client.setServerKeyVerifier(...)指纹校验; uname/uptime/id输出即远程命令真实回显:这次命令以 OS 用户 admin(uid=1000)执行——与服务端第 5.1 节的 root 形成对照,说明"命令身份取决于服务端进程用户"。
八、排查实录:exec 无输出的坑(重要)
第一次搭建时,把 exec 工厂写成下面这样(网上旧示例常见写法),会出现"能登录、命令执行退出码 0 但无任何输出、交互 shell 连上就断":
// 错误写法(实测踩坑)
sshd.setShellFactory(new ProcessShellFactory("/bin/bash", "-i", "-l"));
sshd.setCommandFactory((channel, command) ->
new ProcessShellFactory("/bin/bash", "-c", command).createShell(channel));
开 DEBUG 日志后抓到关键证据:
DEBUG org.apache.sshd.server.shell.ProcessShell - destroy(...) Destroy process for '/bin/sh -c /bin/bash'
实际启动的进程是 /bin/sh -c /bin/bash,根本没带我们的命令!阅读 2.20.0 源码(ProcessShellFactory.java)确认原因:
public class ProcessShellFactory extends AbstractLoggingBean implements ShellFactory {
// ...
protected List<String> resolveEffectiveCommand(
ChannelSession channel, String rawCommand, List<String> parsedElements) {
ValidateUtils.checkNotNullAndNotEmpty(rawCommand, "No command");
// ...
return Arrays.asList(OsUtils.LINUX_SHELL_COMMAND_NAME, "-c", rawCommand);
}
}
结论(2.20.0 实测语义):
ProcessShellFactory只认第一个构造参数,实际执行ProcessBuilder("/bin/sh", "-c", 第一个参数);后面的可变参数elements在源码注释里明确标注 "legacy; unused",完全被忽略;- 因此
new ProcessShellFactory("/bin/bash", "-c", command)变成了执行/bin/sh -c /bin/bash(无参数 bash 读到 EOF 立即退出); - 正确做法:
- exec 通道直接用官方
ProcessShellCommandFactory.INSTANCE——它内部就是拿 exec 命令字符串构造ProcessShellFactory(command, ...),最终执行/bin/sh -c <你输入的命令>; - shell 通道用
InteractiveProcessShellFactory.INSTANCE(它覆写了resolveEffectiveCommand直接返回解析好的 shell 命令数组); - 确需自定义 exec 命令模板时,把整条命令行拼进第一个参数:
new ProcessShellFactory("/bin/bash -c '" + cmd + "'"),不要依赖后续参数。
- exec 通道直接用官方
修正后按第三节代码重写,exec/shell/SFTP 全部恢复正常(见第五、六节实测输出)。
九、常见问题速查
| 现象 | 原因 | 解决 |
|---|---|---|
ssh host cmd 退出码 0 但无输出 | CommandFactory 用了 ProcessShellFactory("/bin/bash","-c",cmd),elements 参数被忽略 | 用 ProcessShellCommandFactory.INSTANCE(见第 8 节) |
| 交互登录立即断开 | 同上,shell 工厂实际只执行了无参 bash | 用 InteractiveProcessShellFactory.INSTANCE |
编译报找不到 SftpSubsystemFactory | SFTP 在独立模块,sshd-core 不含 | 加 sshd-sftp-2.20.0.jar(包名 org.apache.sshd.sftp.server) |
| 下载 sshd-client-2.20.0.jar 得到 404 | 2.x 没有该制品,客户端类在 core | 只需 sshd-core |
| 登录永远失败 | 没设置 PasswordAuthenticator,或回调返回 false | setPasswordAuthenticator(...) |
| 上传文件属主是 root | SSHD 不映射 OS 用户,文件属主=服务进程用户 | 用非特权用户跑服务进程 |
ls -l 属主显示 ? 0 0 | SftpSubsystem 生成 longname 的固有形态 | 正常现象,忽略 |
十、生产化建议
- 进程权限:嵌入式服务端以非 root 用户运行;
ProcessShellFactory的类注释明确警示 SSH 会话与服务进程同权限(无 privilege separation); - 主机密钥:
SimpleGeneratorHostKeyProvider落盘的是 Java 私有序列化格式(.ser),仅自身可读;要跨工具共享主机密钥时改用 PEM 加载(ClassLoadableResourceKeyPairProvider); - 认证加固:把
PasswordAuthenticator回调接到用户数据库/LDAP(官方 sshd-ldap 模块),或启用公钥认证(PublickeyAuthenticator);生产禁用固定密码明文比较; - 主机指纹校验:客户端侧必须替换
AcceptAllServerKeyVerifier(第 7 节 WARN 的来源); - 日志:slf4j-simple 通过
-Dorg.slf4j.simpleLogger.defaultLogLevel=info控制;排查通道问题时临时开 debug,可看到每个进程的启动与销毁记录; - 版本:生产用 2.20.0(2.x 终线);3.0.0-Mx 为里程碑、API 不兼容,暂缓。
十一、小结
- 最小依赖 4 个 jar + SFTP 加 1 个,即可让 Java 进程对外提供完整 SSH 服务:shell(InteractiveProcessShellFactory)、exec(ProcessShellCommandFactory)、SFTP(SftpSubsystemFactory)三通道实测全部打通;
- SSHD 客户端 API 十几行完成"连 OpenSSH → 密码认证 → 执行命令 → 取回输出"闭环;
- 最大坑:
ProcessShellFactory的可变参数是摆设(legacy; unused),exec/shell 通道直接用官方两个 INSTANCE 工厂最稳; - 记住一条铁律:SSH 会话里的命令以服务进程的 OS 用户运行,权限模型由你自己的代码负责。