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 就不会踩坑了。

更多推荐

章节目录