基于Apache MINA SSHD配置及应用
Apache MINA SSHD 是一个基于 Java 的 SSH 服务器和客户端实现,它是 Apache MINA 项目的一部分,提供了完整的 SSH 协议支持。
主要特性
-
SSH 协议支持:
-
支持 SSH2 协议
-
兼容大多数 SSH 客户端
-
支持多种加密算法和密钥交换方法
-
-
服务器功能:
-
可嵌入的 SSH 服务器
-
支持密码认证和公钥认证
-
支持端口转发
-
可自定义的 shell 和命令执行
-
-
客户端功能:
-
完整的 SSH 客户端实现
-
支持交互式和非交互式会话
-
支持 SCP 和 SFTP 文件传输
-
-
具体配置
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.apache.sshd.sftp.common.SftpConstants;
import org.springframework.lang.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;/*** SshClient服务类** @author gengzhy* @since 2025/5/29 16:57*/
@Slf4j
public class SshClientService {private final SshClient sshClient;private final SftpProperties properties;public SshClientService(SshClient sshClient, SftpProperties properties) {this.sshClient = sshClient;this.properties = properties;}/*** 读取文件** @param filePath - 文件路径* @return - Bytes*/public byte[] readOnce(@NonNull String filePath) throws IOException {return executeInternal(session -> {try (SftpClient sftpClient = sftpClient(session)) {SftpClient.Attributes stat = beforeReadCheck(sftpClient, filePath);try (InputStream in = new BufferedInputStream(sftpClient.read(filePath))) {ByteArrayOutputStream out = new ByteArrayOutputStream((int) stat.getSize());byte[] buffer = new byte[8192];int read;while ((read = in.read(buffer, 0, buffer.length)) != -1) {out.write(buffer, 0, read);}return out.toByteArray();}} catch (IOException e) {Throwable ex = getRootCause(e);log.error("sftp 读取文件异常: {}", ex.getMessage(), ex);throw new RuntimeException(ex.getMessage(), ex);}});}/*** 读取文件** @param filePath - 文件路径* @return - Bytes*/public List<String> readLines(@NonNull String filePath) throws IOException {return readLines(filePath, null);}/*** 读取文件** @param filePath - 文件路径* @param lineConsumer - 读取的数据行消费者* @return - Bytes*/public List<String> readLines(@NonNull String filePath, Consumer<String> lineConsumer) throws IOException {return executeInternal(session -> {try (SftpClient sftpClient = sftpClient(session)) {beforeReadCheck(sftpClient, filePath);try (BufferedReader br = new BufferedReader(new InputStreamReader(sftpClient.read(filePath), StandardCharsets.UTF_8))) {List<String> data = new ArrayList<>();String line;while ((line = br.readLine()) != null) {data.add(line);if (lineConsumer != null) {lineConsumer.accept(line);}}return data;}} catch (IOException e) {Throwable ex = getRootCause(e);log.error("sftp 读取文件异常: {}", ex.getMessage(), ex);throw new RuntimeException(ex.getMessage(), ex);}});}/*** 创建文件** @param filePath - 文件路径*/public void writeOnce(@NonNull String filePath, byte[] data) throws IOException {executeInternal(session -> {try (SftpClient sftpClient = sftpClient(session)) {mkDirsIfNotExists(sftpClient, getFileParentPath(filePath));try (SftpClient.CloseableHandle handle = sftpClient.open(filePath, SftpClient.OpenMode.Create, SftpClient.OpenMode.Write, SftpClient.OpenMode.Read)) {sftpClient.write(handle, 0L, data);}return null;} catch (IOException e) {Throwable ex = getRootCause(e);log.error("sftp 创建文件异常: {}", ex.getMessage(), ex);throw new RuntimeException(ex.getMessage(), ex);}});}/*** 递归创建目录(等效于 mkdir -p)** @param sftpClient SFTP 客户端* @param dirPath 目标路径(如 "/a/b/c")*/public void mkDirsIfNotExists(SftpClient sftpClient, String dirPath) throws IOException {if (dirPath == null) {return;}String[] parts = dirPath.split("/");StringBuilder currentPath = new StringBuilder();for (String part : parts) {if (part.isEmpty()) {continue;}currentPath.append("/").append(part);String path = currentPath.toString();if (stat(sftpClient, path) == null) {sftpClient.mkdir(path);}}}/*** 提供一个便于扩展的内容部执行方法,主要是基于{@link ClientSession}对象的一些列操作* <p>* 如:* 创建sftp客户端对象:{@link SftpClientFactory#instance()#createSftpClient(ClientSession)}* 创建shell执行命令对象:{@link ClientSession#createExecChannel(String)}}** @param execCall - 基于{@link ClientSession}对象的回调* @param <T> - 返回数据类型* @return - obj*/public <T> T executeInternal(Function<ClientSession, T> execCall) throws IOException {try (ClientSession session = session()) {return execCall != null ? execCall.apply(session) : null;} catch (IOException e) {log.error("创建ssh会话异常·: {}", e.getMessage(), e);throw new IOException("创建ssh会话异常: " + e.getMessage(), e);}}private SftpClient.Attributes beforeReadCheck(SftpClient sftpClient, String filePath) {SftpClient.Attributes stat = stat(sftpClient, filePath);if (stat == null) {throw new RuntimeException("文件不存在【{" + filePath + "}】");}Asserts.isTrue(stat.isRegularFile(), "不支持的文件类型:" + FileType.getByActualType(stat.getType()));Asserts.isTrue(stat.getSize() <= Integer.MAX_VALUE, "文件过大");return stat;}private String getFileParentPath(String filePath) {String path = filePath.replaceAll("[\\\\/]+", "/");int index = path.lastIndexOf('/');return index == -1 ? null : path.substring(0, index);}/*** 获取文件信息,不存在返回null** @param sftpClient - sftp客户端* @param filePath - 文件路径*/private SftpClient.Attributes stat(SftpClient sftpClient, String filePath) {try {return sftpClient.stat(filePath);} catch (IOException e) {return null;}}private SftpClient sftpClient(ClientSession session) throws IOException {return SftpClientFactory.instance().createSftpClient(session);}private ClientSession session() throws IOException {synchronized (sshClient) {if (!sshClient.isStarted()) {sshClient.start();}}ClientSession session = sshClient.connect(properties.getUsername(), properties.getHost(), properties.getPort()).verify(properties.getConnectTimeout()).getSession();session.addPasswordIdentity(properties.getPassword());session.auth().verify(properties.getAuthTimeout());return session;}private Throwable getRootCause(Throwable ex) {while (ex.getCause() != null) {ex = ex.getCause();}return ex;}@Getter@AllArgsConstructorprivate enum FileType {file(SftpConstants.SSH_FILEXFER_TYPE_REGULAR),dir(SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY),symlink(SftpConstants.SSH_FILEXFER_TYPE_SYMLINK),special_file(SftpConstants.SSH_FILEXFER_TYPE_SPECIAL),SOCKET(SftpConstants.SSH_FILEXFER_TYPE_SOCKET),unknown(SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN),;private final int actualType;public static FileType getByActualType(int actualType) {return Arrays.stream(FileType.values()).filter(item -> item.actualType == actualType).findFirst().orElseThrow(() -> new RuntimeException("Unknown file type: " + actualType));}}
}
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;@Getter
@Setter
@ConfigurationProperties(prefix = "sftp")
public class SftpProperties {/*** 主机IP*/private String host = "127.0.0.1";/*** 主机端口,默认22*/private int port = 22;/*** 用户名*/private String username = "";/*** 登录密码*/private String password = "";/*** 连接超时(ms),默认5000ms*/private Duration connectTimeout = Duration.ofMillis(5000);/*** 认证超时(ms),默认10000ms*/private Duration authTimeout = Duration.ofMillis(10000);
}
import org.apache.sshd.client.SshClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@ConditionalOnClass(SshClient.class)
@Configuration
@EnableConfigurationProperties(SftpProperties.class)
public class SshClientConfiguration {@BeanSshClientService sshClientService(SftpProperties properties) {return new SshClientService(SshClient.setUpDefaultClient(), properties);}
}
sftp:host: 127.0.0.1port: 22username: demopassword: pwdconnect-timeout: 5sauth-timeout: 5s