jsch详解

🧠 一、JSch 的核心概念

  1. 本质与作用
    JSch 是一个纯 Java 实现的 SSH2 协议库,支持连接 SSH 服务器并执行命令、传输文件(SFTP/SCP)、端口转发、X11 转发等功能。它无需依赖本地 OpenSSH 环境,适合集成到 Java 应用中实现自动化运维或安全通信。
    典型场景:远程服务器管理、安全文件传输、自动化脚本执行、通过跳板机访问内网服务。

  2. 核心对象模型

    • JSch:入口类,用于创建会话和配置全局参数。
    • Session:代表一个 SSH 连接会话,管理认证和通道创建。
    • Channel:会话中打开的通道类型,如 ChannelShell(交互式 Shell)、ChannelExec(单命令执行)、ChannelSftp(文件传输)。

⚙️ 二、核心功能与技术特性

  1. 认证方式
    JSch 支持 4 种认证机制,实际开发中常见的是前两种:

    • 密码认证session.setPassword("password")
    • 公私钥认证jsch.addIdentity("~/.ssh/id_rsa")(推荐免密登录)
    • 键盘交互式(keyboard-interactive
    • GSSAPI(gss-api-with-mic,适用于企业级域认证)。
  2. 文件传输(SFTP)
    通过 ChannelSftp 类实现,支持以下操作:

    ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
    sftp.connect();
    sftp.put("local.txt", "/remote/path/"); // 上传
    sftp.get("/remote/file.txt", "local/"); // 下载
    sftp.ls("/dir");                        // 列目录
    sftp.rm("file.txt");                    // 删除文件
    

    传输模式

    • OVERWRITE(默认覆盖)
    • RESUME(断点续传)
    • APPEND(追加内容)。
  3. 端口转发

    • 本地转发(访问本地端口 → 转发到远程服务):
      session.setPortForwardingL(8080, "remote-host", 3306); // 本地 8080 → 远程 MySQL
      
    • 远程转发(暴露本地服务到远程端口):
      session.setPortForwardingR(9090, "localhost", 3000); // 远程 9090 → 本地 Web 服务
      
    • 动态转发(SOCKS 代理):
      session.setPortForwardingD(1080); // 创建 SOCKS5 代理
      

🛠️ 三、基础使用步骤(附代码)

  1. 添加 Maven 依赖

    <dependency>
        <groupId>com.jcraft</groupId>
        <artifactId>jsch</artifactId>
        <version>0.1.55</version>
    </dependency>
    
  2. 建立连接并执行命令

    import com.jcraft.jsch.*;
    
    public class JSchDemo {
        public static void main(String[] args) {
            String user = "root";
            String host = "192.168.1.100";
            int port = 22;
            String password = "pass123";
    
            try {
                JSch jsch = new JSch();
                Session session = jsch.getSession(user, host, port);
                session.setPassword(password);
                
                // 关闭首次连接的主机密钥确认(生产环境慎用!)
                Properties config = new Properties();
                config.put("StrictHostKeyChecking", "no");
                session.setConfig(config);
                
                session.connect();
                
                // 执行单条命令
                ChannelExec channel = (ChannelExec) session.openChannel("exec");
                channel.setCommand("ls -l /tmp");
                channel.connect();
                
                // 读取命令输出
                InputStream in = channel.getInputStream();
                byte[] buffer = new byte[1024];
                while (in.read(buffer) > 0) {
                    System.out.println(new String(buffer));
                }
                
                channel.disconnect();
                session.disconnect();
            } catch (JSchException | IOException e) {
                e.printStackTrace();
            }
        }
    }
    

🔐 四、安全实践与关键配置

  1. 主机密钥检查(StrictHostKeyChecking

    • ask(默认):首次连接提示用户确认密钥(不适用于自动化)。
    • no:自动接受新密钥(仅测试环境使用)。
    • yes:严格匹配,密钥不匹配则拒绝连接(生产环境推荐)。

    📌 建议:预置已知主机密钥到 ~/.ssh/known_hosts,避免关闭检查。

  2. 强化安全性的措施

    • 禁用密码登录:在 sshd_config 设置 PasswordAuthentication no
    • 私钥保护:为私钥设置密码短语,并使用 ssh-agent 管理。
    • 防火墙限制:仅允许可信 IP 访问 SSH 端口。

⚠️ 五、常见问题与调试

  • 连接超时:检查网络、防火墙规则,或调整 session.setTimeout(30000)
  • 认证失败
    • 公私钥不匹配:确认公钥已添加到目标服务器的 ~/.ssh/authorized_keys
    • 权限问题:确保 authorized_keys 权限为 600.ssh 目录权限为 700
  • 调试日志:启用详细日志定位问题:
    JSch.setLogger(new Logger() { 
        public void log(int level, String message) { 
            System.out.println("JSch: " + message);
        }
    });
    

六、我的工具类

package com.huilian.charge.amysql_back.util;

import com.jcraft.jsch.*;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Vector;

/**
 * JSch 工具类,支持 try-with-resources,线程安全,适用于 SFTP 文件下载和命令执行。
 */
public class JSchUtil implements AutoCloseable {

    private final Session session;
    private ChannelExec channelExec;
    private ChannelSftp channelSftp;
    private ChannelShell channelShell;

    /**
     * 构造函数:连接远程服务器
     *
     * @param host     主机地址
     * @param port     端口
     * @param username 用户名
     * @param password 密码
     * @throws JSchException 连接失败时抛出
     */
    public JSchUtil(String host, int port, String username, String password) throws JSchException {
        JSch jsch = new JSch();
        try {
            session = jsch.getSession(username, host, port);
            session.setPassword(password);

            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            session.setConfig(config);
            session.setTimeout(30000); // 30s 超时
            session.connect();
        } catch (JSchException e) {
            throw new JSchException("SSH 连接失败: " + e.getMessage(), e);
        }
    }

    /**
     * 执行单条命令
     *
     * @param command 命令内容
     * @return 输出结果
     * @throws JSchException 执行失败时抛出
     */
    public String executeCommand(String command) throws JSchException {
        if (session == null || !session.isConnected()) {
            throw new JSchException("SSH session 未连接");
        }

        ChannelExec channel = null;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            channel = (ChannelExec) session.openChannel("exec");
            channel.setCommand(command);
            channel.setInputStream(null);
            channel.setOutputStream(out);

            InputStream errStream = channel.getErrStream();

            channel.connect();

            byte[] buffer = new byte[1024];
            while (!channel.isClosed()) {
                while (errStream.available() > 0) {
                    int len = errStream.read(buffer, 0, buffer.length);
                    if (len < 0) break;
                    System.err.write(buffer, 0, len);
                }
                Thread.sleep(100);
            }

            return out.toString(StandardCharsets.UTF_8.name());
        } catch (Exception e) {
            throw new JSchException("执行命令失败: " + command, e);
        } finally {
            if (channel != null && channel.isConnected()) {
                channel.disconnect();
            }
        }
    }

    /**
     * 执行多条 Shell 命令
     *
     * @param cmds 命令列表
     * @return 输出结果
     * @throws JSchException 执行失败时抛出
     */
    public List<String> execShellCommands(List<String> cmds) throws JSchException {
        if (session == null || !session.isConnected()) {
            throw new JSchException("SSH session 未连接");
        }

        List<String> result = new ArrayList<>();
        ChannelShell channel = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;

        try {
            channel = (ChannelShell) session.openChannel("shell");
            channel.setPty(true);
            inputStream = channel.getInputStream();
            outputStream = channel.getOutputStream();
            PrintWriter printWriter = new PrintWriter(outputStream);

            channel.connect();

            for (String cmd : cmds) {
                printWriter.println(cmd);
            }
            // 发送 exit 命令,通知远程 shell 结束
            printWriter.println("exit");
            printWriter.flush();

            byte[] tmp = new byte[1024];
            while (true) {
                while (inputStream.available() > 0) {
                    int i = inputStream.read(tmp, 0, 1024);
                    if (i < 0) break;
                    String line = new String(tmp, 0, i, StandardCharsets.UTF_8);
                    result.add(line);
                }
                if (channel.isClosed()) break;
                Thread.sleep(500);
            }
        } catch (Exception e) {
            throw new JSchException("执行 Shell 命令失败", e);
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException ignored) {
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
            if (channel != null && channel.isConnected()) {
                channel.disconnect();
            }
        }

        return result;
    }

    /**
     * 下载远程文件到 HttpServletResponse(浏览器下载)
     *
     * @param remotePath 远程路径
     * @param fileName   下载后显示的文件名
     * @param response   HttpServletResponse
     * @throws JSchException 下载失败
     */
    public void downloadFile(String remotePath, String fileName, HttpServletResponse response) throws JSchException {
        if (session == null || !session.isConnected()) {
            throw new JSchException("SSH session 未连接");
        }

        ChannelSftp sftp = null;
        InputStream in = null;
        ServletOutputStream out = null;

        try {
            sftp = (ChannelSftp) session.openChannel("sftp");
            sftp.connect();

            in = sftp.get(remotePath);

            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setContentType("application/octet-stream");
            String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment; filename=" + encodedName);

            out = response.getOutputStream();

            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            out.flush();
        } catch (Exception e) {
            throw new JSchException("文件下载失败: " + remotePath, e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ignored) {
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ignored) {
                }
            }
            if (sftp != null && sftp.isConnected()) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 获取远程目录下文件列表
     *
     * @param path 远程路径
     * @return 文件名列表
     * @throws JSchException 获取失败
     */
    public List<String> listFiles(String path) throws JSchException {
        if (session == null || !session.isConnected()) {
            throw new JSchException("SSH session 未连接");
        }

        List<String> files = new ArrayList<>();
        ChannelSftp sftp = null;

        try {
            sftp = (ChannelSftp) session.openChannel("sftp");
            sftp.connect();

            Vector<?> fileList = sftp.ls(path);
            if (fileList != null) {
                for (Object item : fileList) {
                    if (item instanceof ChannelSftp.LsEntry) {
                        files.add(((ChannelSftp.LsEntry) item).getFilename());
                    }
                }
            }
        } catch (Exception e) {
            throw new JSchException("获取文件列表失败: " + path, e);
        } finally {
            if (sftp != null && sftp.isConnected()) {
                sftp.disconnect();
            }
        }

        return files;
    }

    /**
     * 支持断点续传的文件下载(HTTP Range)
     *
     * @param remotePath 远程文件路径
     * @param fileName   下载时显示的文件名
     * @param request    HttpServletRequest
     * @param response   HttpServletResponse
     * @throws JSchException 下载失败
     */
    public void downloadFileWithResume(String remotePath, String fileName,
                                       HttpServletRequest request, HttpServletResponse response)
            throws JSchException {
        if (session == null || !session.isConnected()) {
            throw new JSchException("SSH session 未连接");
        }

        ChannelSftp sftp = null;
        InputStream in = null;
        ServletOutputStream out = null;

        try {
            sftp = (ChannelSftp) session.openChannel("sftp");
            sftp.connect();

            // 获取远程文件大小
            long fileLength = sftp.lstat(remotePath).getSize();
            String rangeHeader = request.getHeader("Range");

            // 设置响应头
            response.setContentType("application/octet-stream");
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment; filename=" + encodedName);
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("Content-Length", String.valueOf(fileLength));

            long start = 0;
            long end = fileLength - 1;
            boolean partial = false;

            // 解析 Range 请求头
            if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
                String[] ranges = rangeHeader.substring(6).split("-");
                start = Long.parseLong(ranges[0]);
                if (ranges.length > 1 && !ranges[1].isEmpty()) {
                    end = Long.parseLong(ranges[1]);
                }
                partial = true;
            }

            long contentLength = end - start + 1;

            if (partial) {
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206 Partial Content
                response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
                response.setHeader("Content-Length", String.valueOf(contentLength));
            }

            // 打开输入流并跳转到指定位置
            in = sftp.get(remotePath);
            if (in.skip(start) < start) {
                throw new IOException("无法跳转到指定偏移量: " + start);
            }

            out = response.getOutputStream();

            byte[] buffer = new byte[4096];
            long remaining = contentLength;
            int bytesRead;

            while (remaining > 0 && (bytesRead = in.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) {
                out.write(buffer, 0, bytesRead);
                remaining -= bytesRead;
            }

            out.flush();

        } catch (Exception e) {
            throw new JSchException("断点续传下载失败: " + remotePath, e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ignored) {
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ignored) {
                }
            }
            if (sftp != null && sftp.isConnected()) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 关闭所有资源
     */
    @Override
    public void close() {
        if (channelExec != null && channelExec.isConnected()) {
            channelExec.disconnect();
        }
        if (channelSftp != null && channelSftp.isConnected()) {
            channelSftp.disconnect();
        }
        if (channelShell != null && channelShell.isConnected()) {
            channelShell.disconnect();
        }
        if (session != null && session.isConnected()) {
            session.disconnect();
        }
    }
}


💎 总结

JSch 让 Java 应用轻松集成企业级 SSH 功能,核心在于:

  • ✅ 通过 Session 管理连接,Channel 实现具体操作(如 SFTP、命令执行)。
  • 优先使用公私钥认证,结合 StrictHostKeyChecking=yes 提升安全性。
  • ✅ 文件传输选 ChannelSftp,支持断点续传与进度监控(SftpProgressMonitor)。
  • ⚠️ 避免内网测试配置(如关闭主机检查)泄露到生产环境。

如需实现复杂功能(如 X11 转发、HTTP 代理),可参考 官方示例。对性能敏感场景,建议复用 Session 避免频繁重建连接。

posted @ 2025-07-08 18:29  bigger_apple  阅读(554)  评论(0)    收藏  举报