Apache SSHD 2.20 嵌入式集成使用指南

Apache SSHD 2.20 从 0 到 1 嵌入 Java 应用:最小依赖准备、嵌入式 SSH 服务端搭建、shell/exec/SFTP 三通道实测、SSHD 客户端连接 OpenSSH 实测,以及 CommandFactory 配错导致 exec 无输出的排查实录

Apache SSHDSSHDJava2026/10/15

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(javac 17.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 账号无关)

完成后你将得到:

  1. 一个监听 2222 端口的嵌入式 SSH 服务端(Java 进程内);
  2. 系统 ssh 可登录执行命令、交互 shell;
  3. 系统 sftp 可上传下载文件;
  4. 一个 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(交互登录)shellsetShellFactory(...)InteractiveProcessShellFactory.INSTANCE
ssh host "cmd"(远程命令)execsetCommandFactory(...)ProcessShellCommandFactory.INSTANCE
sftp/scp 文件传输subsystemsetSubsystemFactories(...)第 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)

三个关键点:

  1. Authed as admin, server version=SSH-2.0-OpenSSH_8.9p1:认证成功且取到对端版本串;
  2. WARN AcceptAllServerKeyVerifier ... unverified EC key:演示代码用了"不校验主机指纹"的默认 verifier,生产必须替换为 client.setServerKeyVerifier(...) 指纹校验;
  3. 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 实测语义):

  1. ProcessShellFactory 只认第一个构造参数,实际执行 ProcessBuilder("/bin/sh", "-c", 第一个参数);后面的可变参数 elements 在源码注释里明确标注 "legacy; unused",完全被忽略;
  2. 因此 new ProcessShellFactory("/bin/bash", "-c", command) 变成了执行 /bin/sh -c /bin/bash(无参数 bash 读到 EOF 立即退出);
  3. 正确做法:
    • exec 通道直接用官方 ProcessShellCommandFactory.INSTANCE——它内部就是拿 exec 命令字符串构造 ProcessShellFactory(command, ...),最终执行 /bin/sh -c <你输入的命令>;
    • shell 通道用 InteractiveProcessShellFactory.INSTANCE(它覆写了 resolveEffectiveCommand 直接返回解析好的 shell 命令数组);
    • 确需自定义 exec 命令模板时,把整条命令行拼进第一个参数:new ProcessShellFactory("/bin/bash -c '" + cmd + "'"),不要依赖后续参数。

修正后按第三节代码重写,exec/shell/SFTP 全部恢复正常(见第五、六节实测输出)。

九、常见问题速查

现象原因解决
ssh host cmd 退出码 0 但无输出CommandFactory 用了 ProcessShellFactory("/bin/bash","-c",cmd),elements 参数被忽略用 ProcessShellCommandFactory.INSTANCE(见第 8 节)
交互登录立即断开同上,shell 工厂实际只执行了无参 bash用 InteractiveProcessShellFactory.INSTANCE
编译报找不到 SftpSubsystemFactorySFTP 在独立模块,sshd-core 不含加 sshd-sftp-2.20.0.jar(包名 org.apache.sshd.sftp.server)
下载 sshd-client-2.20.0.jar 得到 4042.x 没有该制品,客户端类在 core只需 sshd-core
登录永远失败没设置 PasswordAuthenticator,或回调返回 falsesetPasswordAuthenticator(...)
上传文件属主是 rootSSHD 不映射 OS 用户,文件属主=服务进程用户用非特权用户跑服务进程
ls -l 属主显示 ? 0 0SftpSubsystem 生成 longname 的固有形态正常现象,忽略

十、生产化建议

  1. 进程权限:嵌入式服务端以非 root 用户运行;ProcessShellFactory 的类注释明确警示 SSH 会话与服务进程同权限(无 privilege separation);
  2. 主机密钥:SimpleGeneratorHostKeyProvider 落盘的是 Java 私有序列化格式(.ser),仅自身可读;要跨工具共享主机密钥时改用 PEM 加载(ClassLoadableResourceKeyPairProvider);
  3. 认证加固:把 PasswordAuthenticator 回调接到用户数据库/LDAP(官方 sshd-ldap 模块),或启用公钥认证(PublickeyAuthenticator);生产禁用固定密码明文比较;
  4. 主机指纹校验:客户端侧必须替换 AcceptAllServerKeyVerifier(第 7 节 WARN 的来源);
  5. 日志:slf4j-simple 通过 -Dorg.slf4j.simpleLogger.defaultLogLevel=info 控制;排查通道问题时临时开 debug,可看到每个进程的启动与销毁记录;
  6. 版本:生产用 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 用户运行,权限模型由你自己的代码负责。