Spring Boot + sshd-sftp:SSH 命令與文件傳輸實(shí)踐
前言
在現(xiàn)代分布式系統(tǒng)中,服務(wù)器間的遠(yuǎn)程操作與文件傳輸是常見(jiàn)需求。SSH作為一種安全的網(wǎng)絡(luò)協(xié)議,為遠(yuǎn)程登錄和文件傳輸提供了可靠保障。
環(huán)境準(zhǔn)備
sshd-sftp是Apache MINA項(xiàng)目旗下的SSH服務(wù)器組件,支持SSHv2協(xié)議,提供了豐富的API用于構(gòu)建SSH客戶端和服務(wù)器。相比傳統(tǒng)的JSch庫(kù),sshd-sftp具有更活躍的社區(qū)維護(hù)和更完善的功能支持,尤其在處理大文件傳輸和并發(fā)連接時(shí)表現(xiàn)更優(yōu)。
案例
效果圖
圖片
核心依賴
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>2.10.0</version>
</dependency>SSH 連接管理
建立和管理SSH連接是所有操作的基礎(chǔ),合理的連接池設(shè)計(jì)能有效提升系統(tǒng)性能。
創(chuàng)建SshClientUtil工具類管理連接生命周期:
import lombok.extern.slf4j.Slf4j;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class SshClientUtil {
@Value("${ssh.host:182.168.1.101}")
private String host;
@Value("${ssh.port:22}")
private int port;
@Value("${ssh.username:root}")
private String username;
@Value("${ssh.password:root}")
private String password;
@Value("${ssh.timeout:5000}")
private int timeout;
private SshClient client;
private ClientSession session;
/**
* 建立SSH連接
*/
public void connect() throws IOException {
if (session != null && session.isOpen()) {
return;
}
client = SshClient.setUpDefaultClient();
client.start();
session = client.connect(username, host, port)
.verify(timeout)
.getSession();
session.addPasswordIdentity(password);
session.auth().verify(timeout);
log.info("SSH連接成功");
}
/**
* 斷開(kāi)SSH連接
*/
public void disconnect() throws IOException {
if (session != null && session.isOpen()) {
session.close();
}
if (client != null && client.isStarted()) {
client.stop();
}
}
public ClientSession getSession() throws IOException {
if (session == null || session.isEmpty() ||session.isClosed()) {
connect();
}
return session;
}
}遠(yuǎn)程命令執(zhí)行
通過(guò)SSH協(xié)議執(zhí)行遠(yuǎn)程命令是服務(wù)器管理的常用功能,需要處理命令輸出和錯(cuò)誤信息。
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class RemoteCommandService {
@Autowired
private SshClientUtil sshClientUtil;
@Value("${ssh.charset:UTF-8}")
private String charset;
/**
* 執(zhí)行單條SSH命令
*
* @param command 命令字符串
* @return 命令輸出結(jié)果
*/
public String executeCommand(String command) throws IOException {
try (ChannelExec channel = sshClientUtil.getSession().createExecChannel(command)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
channel.setOut(outputStream);
channel.setErr(errorStream);
channel.open();
channel.waitFor(Arrays.asList(ClientChannelEvent.CLOSED), 0);
int exitStatus = channel.getExitStatus();
if (exitStatus != 0) {
String errorMsg = new String(errorStream.toByteArray(), charset);
throw new RuntimeException("命令執(zhí)行失敗: " + errorMsg);
}
return new String(outputStream.toByteArray(), charset);
}
}
/**
* 執(zhí)行多條命令(按順序執(zhí)行)
*
* @param commands 命令列表
* @return 命令輸出結(jié)果列表
*/
public List<String> executeCommands(List<String> commands) throws IOException {
List<String> results = new ArrayList<>();
for (String cmd : commands) {
results.add(executeCommand(cmd));
}
return results;
}
}文件上傳與下載
SFTP是基于SSH的安全文件傳輸協(xié)議,相比FTP具有更高的安全性。
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.apache.sshd.sftp.common.SftpException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class SftpFileService {
@Autowired
private SshClientUtil sshClientUtil;
@Value("${ssh.charset:UTF-8}")
private String charset;
/**
* 上傳本地文件到遠(yuǎn)程服務(wù)器
*
* @param localFilePath 本地文件路徑
* @param remoteDir 遠(yuǎn)程目錄路徑
*/
public void uploadFile(String localFilePath, String remoteDir) throws IOException {
try (SftpClient client = SftpClientFactory.instance().createSftpClient(sshClientUtil.getSession());
SftpClient.CloseableHandle handle = client.open(remoteDir, SftpClient.OpenMode.Write, SftpClient.OpenMode.Create)) {
// 上傳文件
Path local = Paths.get(localFilePath);
client.write(handle, 0, Files.readAllBytes(local));
}
}
/**
* 從遠(yuǎn)程服務(wù)器下載文件
*
* @param remoteFilePath 遠(yuǎn)程文件路徑
* @param localDir 本地目錄路徑
*/
public void downloadFile(String remoteFilePath, String localDir) throws IOException {
try (SftpClient client = SftpClientFactory.instance().createSftpClient(sshClientUtil.getSession());
SftpClient.CloseableHandle handle = client.open(remoteFilePath, SftpClient.OpenMode.Read);
OutputStream out = Files.newOutputStream(Paths.get(localDir))) {
long size = client.stat(handle).getSize();
byte[] buffer = new byte[8192];
for (long offset = 0; offset < size; ) {
int len = client.read(handle, offset, buffer, 0, buffer.length);
out.write(buffer, 0, len);
offset += len;
}
}
}
/**
* 遞歸創(chuàng)建遠(yuǎn)程目錄
*/
private void mkdirs(SftpClient client, String dir) throws IOException {
String[] dirs = dir.split("/");
String currentDir = "";
for (String d : dirs) {
if (d.isEmpty()) continue;
currentDir += "/" + d;
try {
client.stat(currentDir); // 檢查目錄是否存在
} catch (IOException e) {
client.mkdir(currentDir); // 不存在則創(chuàng)建
}
}
}
}


























