Java用JSch实现SFTP文件上传
JSch 是纯 Java 实现的 SSH 库,能安全上传文件到 SFTP 服务器。但原始代码有坑:FTP 和 SFTP 混称、不验证 host key、资源不关闭、异常吞掉。正确做法是验证指纹、用 try-with-resources、启用 StrictHostKeyChecking。
JSch 是 Java 里用来处理 SSH 和 SFTP 的纯 Java 库。很多人第一次用的时候容易采坑,因为网上的教程要么不验证 host key(安全隐患),要么资源泄漏。这里就讲讲正确的用法。
SFTP vs FTP
首先澄清一下概念:
- FTP:File Transfer Protocol,明文传输,超过 50 年的古董协议。
- SFTP:SSH File Transfer Protocol,基于 SSH,加密传输,现在的标准。
FTP 基本不用了,安全风险太大。SFTP 是通过 SSH 隧道传输的,所以天然支持密钥认证和加密。
JSch 的名字里没有 SSH,就是因为它既支持 SSH,也支持 SFTP(通过 SSH 隧道)。
Maven 依赖
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
最小化的 SFTP 上传
import com.jcraft.jsch.*;
public class SftpUploader {
public static void uploadFile(String host, String user, String password,
String remoteFile, String localFile)
throws JSchException, SftpException {
JSch jsch = new JSch();
Session session = jsch.getSession(user, host, 22);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "yes"); // 验证主机
session.connect();
ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
sftp.connect();
sftp.put(localFile, remoteFile); // 上传
sftp.disconnect();
session.disconnect();
}
}
但这段代码还有几个问题,继续往下看。
原始代码常见的坑
坑一:不验证 Host Key(安全隐患)
// ❌ 危险的写法
session.setConfig("StrictHostKeyChecking", "no"); // 关闭验证,容易被中间人攻击
StrictHostKeyChecking = "no" 意思是"不验证服务器身份",任何人声称自己是 sftp.example.com 就信。这是个严重的安全漏洞。
正确做法:验证指纹或提前加入已知主机。
// ✅ 方式一:验证指纹(需要提前获得服务器 SSH 公钥指纹)
KnownHosts knownHosts = new KnownHosts("/path/to/.ssh/known_hosts");
jsch.setKnownHosts(knownHosts);
session.setConfig("StrictHostKeyChecking", "yes"); // 启用验证
// ✅ 方式二:直接提供公钥文件
jsch.setKnownHosts("/home/user/.ssh/known_hosts"); // Linux/Mac
session.setConfig("StrictHostKeyChecking", "yes");
// ✅ 方式三:允许首次连接时询问(开发环境可以用)
session.setConfig("StrictHostKeyChecking", "ask"); // 首次连接会提示
实际用下来,方式二最常用:把服务器的 known_hosts 放在固定位置,JSch 会验证。
坑二:FTP 和 SFTP 混淆
很多人看网上的教程,打开的是 FTP channel,不是 SFTP:
// ❌ 不要这样,这是传统 FTP
Channel channel = session.openChannel("ftp");
// ✅ 应该用 sftp
ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
FTP 用的是明文,SFTP 用的是 SSH 加密通道。必须用 sftp。
坑三:异常吞掉,连接没关
原始代码常见的模式:
// ❌ 异常会导致资源泄漏
Session session = jsch.getSession(user, host, 22);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "yes");
session.connect();
ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
sftp.connect();
if (sftp.ls(remoteDir).isEmpty()) { // 这里可能抛异常
sftp.mkdir(remoteDir);
}
sftp.put(localFile, remoteFile); // 这里也可能抛异常
sftp.disconnect(); // 异常的话永远执行不到
session.disconnect();
如果中间任何一步异常了,disconnect 永远不会执行,服务器会有一堆僵尸连接。
正确做法:用 try-with-resources(Java 7+)。
// ✅ 自动关闭资源
try (Session session = jsch.getSession(user, host, 22)) {
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "yes");
session.connect();
// JSch 的 Channel 没实现 AutoCloseable,需要手动包装
ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
try {
sftp.connect();
sftp.put(localFile, remoteFile);
} finally {
sftp.disconnect();
}
} catch (JSchException | SftpException e) {
e.printStackTrace();
}
不过 JSch 的 Channel 没有实现 AutoCloseable,所以我们只能把 Session 用 try-with-resources,Channel 还得手动 finally。
坑四:错误信息不清楚
JSch 的异常有时候很模糊,比如 SftpException.id 是个数字,要自己查文档才知道啥意思。
// ❌ 不知所云
catch (SftpException e) {
System.out.println("Error: " + e.id); // 显示 2,啥意思?
}
// ✅ 要自己翻译
catch (SftpException e) {
String message = getSftpErrorMessage(e.id);
System.out.println("SFTP Error: " + message);
}
private static String getSftpErrorMessage(int id) {
switch (id) {
case 0: return "SSH_FX_OK";
case 1: return "SSH_FX_EOF";
case 2: return "SSH_FX_NO_SUCH_FILE";
case 3: return "SSH_FX_PERMISSION_DENIED";
case 4: return "SSH_FX_FAILURE";
// ...
default: return "Unknown error: " + id;
}
}
正确的封装
import com.jcraft.jsch.*;
import java.io.IOException;
public class SftpClient implements AutoCloseable {
private JSch jsch;
private Session session;
private ChannelSftp sftp;
public SftpClient(String host, String user, String password, String knownHostsPath)
throws JSchException {
jsch = new JSch();
// 加载已知主机文件
if (knownHostsPath != null) {
jsch.setKnownHosts(knownHostsPath);
}
// 建立会话
session = jsch.getSession(user, host, 22);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "yes"); // 验证主机
session.setConfig("PreferredAuthentications", "password");
session.connect();
// 打开 SFTP 通道
sftp = (ChannelSftp) session.openChannel("sftp");
sftp.connect();
}
// 上传文件
public void uploadFile(String localFile, String remoteFile) throws SftpException {
sftp.put(localFile, remoteFile);
}
// 下载文件
public void downloadFile(String remoteFile, String localFile) throws SftpException {
sftp.get(remoteFile, localFile);
}
// 创建目录
public void mkdir(String path) throws SftpException {
try {
sftp.stat(path); // 检查目录是否存在
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
sftp.mkdir(path); // 目录不存在,创建
} else {
throw e;
}
}
}
// 删除文件
public void deleteFile(String remoteFile) throws SftpException {
sftp.rm(remoteFile);
}
// 关闭连接
@Override
public void close() {
if (sftp != null && sftp.isConnected()) {
sftp.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
使用方法:
// 使用
try (SftpClient client = new SftpClient("sftp.example.com", "user", "password",
"/path/to/known_hosts")) {
client.uploadFile("local_file.txt", "/remote/path/file.txt");
client.mkdir("/remote/path/newdir");
} catch (JSchException | SftpException e) {
System.err.println("SFTP operation failed: " + e.getMessage());
e.printStackTrace();
}
使用密钥认证(而不是密码)
很多服务器禁用了密码登录,只允许密钥认证。JSch 也支持:
JSch jsch = new JSch();
// 添加私钥
jsch.addIdentity("/home/user/.ssh/id_rsa"); // 私钥路径
// 或者带密码的私钥
jsch.addIdentity("/home/user/.ssh/id_rsa", "私钥密码");
// 之后就不用 setPassword 了
Session session = jsch.getSession(user, host, 22);
session.setConfig("PreferredAuthentications", "publickey"); // 用公钥认证
session.setConfig("StrictHostKeyChecking", "yes");
session.connect();
常见异常
catch (JSchException e) {
if (e.getMessage().contains("Auth fail")) {
System.out.println("认证失败,检查用户名/密码/密钥");
} else if (e.getMessage().contains("Connection refused")) {
System.out.println("连接被拒绝,检查主机地址和端口");
} else if (e.getMessage().contains("UnknownHostKey")) {
System.out.println("未知主机,检查 known_hosts 文件");
} else {
System.out.println("其他 SSH 错误: " + e.getMessage());
}
}
catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
System.out.println("文件或目录不存在");
} else if (e.id == ChannelSftp.SSH_FX_PERMISSION_DENIED) {
System.out.println("权限不足");
} else {
System.out.println("其他 SFTP 错误: " + e.getMessage());
}
}
小结
JSch 上传文件的关键点:
- 用 SFTP 不要用 FTP(ssh 加密通道)
- 验证 host key,不要关闭检查(安全隐患)
- 用 try-finally 或 try-with-resources 确保连接关闭(资源泄漏)
- 用私钥认证而不是密码认证(更安全)
- 主动处理异常,不要吞掉(便于调试)
掌握这些,用 JSch 就不会踩坑了。
