SFTP和SSH的java实现
sftp与ftp
要谈sftp
(SSH File Transfer Protocol
),首先要谈ftp
(File Transfer Protocol
),大家都知道ftp是文件传输协议,它基于tcp
协议,可以用来发送文件。
sftp与ssh
那sftp
,就是安全(security)的ftp
,因为它是基于ssh
协议,ssh 为 Secure Shell 的缩写,由 IETF 的网络小组(Network Working Group)所制定;SSH 为建立在应用层基础上的安全协议。SSH 是较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题
,ssh
在连接和传送的过程中会加密所有的数据。所以通俗的来讲,通过ssh协议进行文件传输,那就是sftp。
那么如何使用ssh
来实现文件传输呢?熟悉linux的伙伴们应该对ssh
也不陌生,因为linux自带了ssh(嘘!其实我并不熟悉linux,只是知道linux自带了ssh,以前装了没多久就卸了,现在发现会linux逼格会提高不少。一定要再装一个玩一下!)。
遗憾的是,ssh基本上是基于linux和一些客户端安装软件。那么在我们平常的web开发中,要用sftp来传输文件怎么办呢?jsch就是解决办法了。
jsch简介
jsch
是ssh的纯java实现。这么讲有点抽象,通俗说,你在官网上down下来就是一个jar包,引入你的项目,就可以开始使用
- 第一步:首先在maven中央仓库中查一下怎么在pom中依赖,可以点这里。
-
123456
<!-- https:
//mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
- 第二步:创建一个工具类:SFTPClient.java, 实现文件上传、下载、读取等功能:代买如下:
-
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
package
com.zjh.logviewer.ssh;
import
com.zjh.logviewer.model.FileAttri;
import
com.zjh.logviewer.model.Server;
import
java.io.File;
import
java.io.FileInputStream;
import
java.io.IOException;
import
java.io.InputStream;
import
java.nio.charset.Charset;
import
java.util.ArrayList;
import
java.util.Collections;
import
java.util.List;
import
java.util.Vector;
import
org.apache.commons.lang3.ArrayUtils;
import
com.jcraft.jsch.ChannelSftp;
import
com.jcraft.jsch.ChannelSftp.LsEntry;
import
com.jcraft.jsch.JSchException;
import
com.jcraft.jsch.SftpATTRS;
import
com.jcraft.jsch.SftpException;
public
class
SFTPClient
extends
BaseJchClient {
private
ChannelSftp channel;
public
SFTPClient(Server serverInfo)
throws
RemoteAccessException {
super
(serverInfo);
}
private
ChannelSftp openChannel()
throws
RemoteAccessException {
try
{
if
(channel !=
null
) {
if
(channel.isConnected() || !channel.isClosed()) {
channel.disconnect();
channel =
null
;
}
else
{
return
channel;
}
}
conn();
channel = (ChannelSftp) session.openChannel(
"sftp"
);
channel.connect(DEFAULT_CONN_TIMEOUT);
return
channel;
}
catch
(JSchException e) {
throw
new
RemoteAccessException(e);
}
}
/**
* 从sftp服务器下载指定文件到本地指定目录
*
* @param remoteFile 文件的绝对路径+fileName
* @param localPath 本地临时文件路径
* @return
*/
public
boolean
download(String remoteFile, String localPath)
throws
RemoteAccessException {
ChannelSftp sftp =
null
;
try
{
sftp = openChannel();
sftp.get(remoteFile, localPath);
return
true
;
}
catch
(SftpException e) {
logger.error(
"download remoteFile:{},localPath:{}, ex:{}"
, remoteFile, localPath, e);
throw
new
RemoteAccessException(e);
}
}
/**
* 上传文件
*
* @param directory 上传的目录-相对于SFPT设置的用户访问目录, 为空则在SFTP设置的根目录进行创建文件(除设置了服务器全磁盘访问)
* @param uploadFile 要上传的文件全路径
*/
public
boolean
upload(String directory, String uploadFile)
throws
Exception {
ChannelSftp sftp =
null
;
try
{
try
{
sftp = openChannel();
sftp.cd(directory);
// 进入目录
}
catch
(SftpException sException) {
if
(sftp.SSH_FX_NO_SUCH_FILE == sException.id) {
// 指定上传路径不存在
sftp.mkdir(directory);
// 创建目录
sftp.cd(directory);
// 进入目录
}
}
File file =
new
File(uploadFile);
InputStream in =
new
FileInputStream(file);
sftp.put(in, file.getName());
in.close();
}
catch
(Exception e) {
throw
new
Exception(e.getMessage(), e);
}
return
true
;
}
/**
* 读取sftp上指定文件数据
*
* @param remoteFile
* @return
*/
public
byte
[] getFile(String remoteFile)
throws
RemoteAccessException {
ChannelSftp sftp =
null
;
InputStream inputStream =
null
;
try
{
sftp = openChannel();
inputStream = sftp.get(remoteFile);
return
IOHelper.readBytes(inputStream);
}
catch
(SftpException | IOException e) {
logger.error(
"getFile remoteFile:{},ex:{}"
, remoteFile, e);
throw
new
RemoteAccessException(e);
}
finally
{
IOHelper.closeQuietly(inputStream);
}
}
/**
* 读取sftp上指定(文本)文件数据,并按行返回数据集合
*
* @param remoteFile
* @param charset
* @return
*/
public
String getFileContent(String remoteFile, Charset charset)
throws
RemoteAccessException {
ChannelSftp sftp =
null
;
InputStream inputStream =
null
;
try
{
sftp = openChannel();
inputStream = sftp.get(remoteFile);
return
IOHelper.readText(inputStream, charset);
}
catch
(SftpException | IOException e) {
logger.error(
"getFileText remoteFile:{},error:{}"
, remoteFile, e);
throw
new
RemoteAccessException(e);
}
finally
{
IOHelper.closeQuietly(inputStream);
}
}
/**
* 列出指定目录下文件列表
*
* @param remotePath
* @param descendant 是否递归查询子孙目录
* @param excludes 要排除的文件
* @return
*/
public
List<FileAttri> ls(String remotePath,
boolean
descendant, String... excludes)
throws
RemoteAccessException {
ChannelSftp sftp =
null
;
List<FileAttri> lsFiles;
try
{
sftp = openChannel();
lsFiles = ls(sftp, remotePath, descendant, excludes);
}
catch
(SftpException e) {
logger.error(
"ls remotePath:{} , error:{}"
, remotePath, e.getMessage());
if
(
"Permission denied"
.equals(e.getMessage())) {
throw
new
PermissionException(
"没有文件读取权限"
);
}
throw
new
RemoteAccessException(e);
}
if
(lsFiles !=
null
) {
Collections.sort(lsFiles);
}
return
lsFiles;
}
@SuppressWarnings
(
"unchecked"
)
private
List<FileAttri> ls(ChannelSftp sftp, String remotePath,
boolean
descendant, String... excludes)
throws
SftpException {
List<FileAttri> lsFiles =
new
ArrayList<>();
FileAttri tmpFileAttri;
long
tmpSize;
String tmpFullPath;
Vector<LsEntry> vector = sftp.ls(remotePath);
for
(LsEntry entry : vector) {
if
(
"."
.equals(entry.getFilename()) ||
".."
.equals(entry.getFilename())) {
continue
;
}
tmpFullPath = UrlHelper.mergeUrl(remotePath, entry.getFilename());
if
(excludes !=
null
&& ArrayUtils.contains(excludes, tmpFullPath)) {
logger.debug(
"忽略目录:{}"
, tmpFullPath);
continue
;
}
tmpFileAttri =
new
FileAttri();
tmpFileAttri.setNodeName(entry.getFilename());
tmpFileAttri.setLastUpdateDate(entry.getAttrs()
.getATime());
tmpFileAttri.setPath(tmpFullPath);
if
(entry.getAttrs()
.isDir()) {
tmpFileAttri.setDir(
true
);
if
(descendant) {
try
{
List<FileAttri> childs = ls(sftp, tmpFileAttri.getPath(), descendant, excludes);
if
(CollectionHelper.isNotEmpty(childs)) {
tmpFileAttri.addNodes(childs);
}
}
catch
(PermissionException e) {
tmpFileAttri.setNodeName(tmpFileAttri.getNodeName() +
"[无权限]"
);
}
}
}
else
{
tmpFileAttri.setDir(
false
);
tmpSize = entry.getAttrs()
.getSize();
if
(tmpSize <
1024
) {
tmpFileAttri.setSize(entry.getAttrs()
.getSize() +
"B"
);
}
else
if
(tmpSize >=
1024
&& tmpSize <
1048576
) {
tmpFileAttri.setSize(MathHelper.round((entry.getAttrs()
.getSize() / 1024f),
1
) +
"KB"
);
}
else
if
(tmpSize >
1048576
) {
tmpFileAttri.setSize(MathHelper.round((entry.getAttrs()
.getSize() / 1048576f),
2
) +
"MB"
);
}
}
lsFiles.add(tmpFileAttri);
}
return
lsFiles;
}
/**
* 判断文件是否存在
*
* @param filePath
* @return
* @throws RemoteAccessException
*/
public
boolean
isExist(String filePath)
throws
RemoteAccessException {
ChannelSftp sftp =
null
;
try
{
sftp = openChannel();
SftpATTRS attrs = sftp.lstat(filePath);
return
attrs !=
null
;
}
catch
(SftpException e) {
logger.error(
"文件不存在,remotePath:{} , error:{}"
, filePath, e.getMessage());
return
false
;
}
}
@Override
public
void
close()
throws
IOException {
if
(channel !=
null
) {
try
{
channel.disconnect();
}
catch
(Exception e) {
channel =
null
;
}
}
super
.close();
}
}
点击并拖拽以移动
第三步:创建ssh交互的工具类:SSHClient.java, 实现执行单条命令、异步执行命令的功能:代码如下:huo获取
-
import com.zjh.logviewer.model.Server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSchException; public class SSHClient extends BaseJchClient { public SSHClient(Server serverInfo) throws RemoteAccessException { super(serverInfo); } /** * 执行单条命令 * * @param cmd * @return StringBuffer命令结果 * @throws RemoteAccessException */ public StringBuffer exec(String cmd) throws RemoteAccessException { String line = null; BufferedReader reader = null; ChannelExec channelExec = null; StringBuffer resultBuf = new StringBuffer(); InputStream inStream = null; try { channelExec = getChannel(cmd); stopLastCmdThread();// 中断前一个异步命令的执行 inStream = channelExec.getInputStream(); channelExec.connect(); if (inStream != null) { reader = IOHelper.toBufferedReader(inStream, StandardCharsets.UTF_8.name()); while ((line = reader.readLine()) != null) { resultBuf.append(line) .append(Chars.LF); } } } catch (IOException | JSchException e) { logger.error("执行命令异常,ip:{},cmd:{},ex:{}", serverInfo.getIp(), cmd, e); throw new RemoteAccessException(e); } finally { IOHelper.closeQuietly(inStream); if (channelExec != null) { channelExec.disconnect(); } } if (logger.isDebugEnabled()) { logger.debug("{}执行命令:{},结果:{}", serverInfo.getIp(), cmd, resultBuf); } return resultBuf; } ExecAsyncCmdThread cmdThread = null; /** * 异步执行命令 * * @param cmd * @param handler * @throws RemoteAccessException */ public void execAsync(final String cmd, final AsyncCmdCallBack callBack) throws RemoteAccessException { ChannelExec channelExec = getChannel(cmd); stopLastCmdThread(); cmdThread = new ExecAsyncCmdThread(channelExec, callBack); cmdThread.setDaemon(true); cmdThread.start(); } private ChannelExec getChannel(String cmd) throws RemoteAccessException { conn(); try { ChannelExec channel = (ChannelExec) session.openChannel("exec"); channel.setCommand(cmd); channel.setInputStream(null); channel.setErrStream(System.err); return channel; } catch (JSchException e) { throw new RemoteAccessException(e); } } class ExecAsyncCmdThread extends Thread { private AsyncCmdCallBack callBack; private ChannelExec channelExec = null; private volatile boolean isRunning = false; public ExecAsyncCmdThread(final ChannelExec channelExec, AsyncCmdCallBack callBack) { this.channelExec = channelExec; this.callBack = callBack; } @Override public void run() { InputStream inStream = null; String line; BufferedReader reader = null; channelExec.setPty(true); try { inStream = channelExec.getInputStream(); channelExec.connect(); isRunning = true; reader = IOHelper.toBufferedReader(inStream, StandardCharsets.UTF_8.name()); while (isRunning) { while ((line = reader.readLine()) != null) { callBack.hanndle(line); } if (channelExec.isClosed()) { int res = channelExec.getExitStatus(); isRunning = false; System.out.println(String.format("Exit-status: %d,thread:%s", res, Thread.currentThread() .getId())); break; } } } catch (Exception e) { logger.error("", e); } finally { IOHelper.closeQuietly(reader); if (channelExec != null) { channelExec.disconnect(); } isRunning = false; } } public void close() { channelExec.disconnect(); isRunning = false; } } void stopLastCmdThread() { if (cmdThread != null) { cmdThread.close(); } } @Override public void close() throws IOException { stopLastCmdThread(); cmdThread = null; super.close(); } }
第四步:在需要使用的地方,调用此两个类即可
获取项目源码
https://github.com/BigDataAiZq/logviewer
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?