jsch详解
🧠 一、JSch 的核心概念
-
本质与作用
JSch 是一个纯 Java 实现的 SSH2 协议库,支持连接 SSH 服务器并执行命令、传输文件(SFTP/SCP)、端口转发、X11 转发等功能。它无需依赖本地 OpenSSH 环境,适合集成到 Java 应用中实现自动化运维或安全通信。
典型场景:远程服务器管理、安全文件传输、自动化脚本执行、通过跳板机访问内网服务。 -
核心对象模型
JSch:入口类,用于创建会话和配置全局参数。Session:代表一个 SSH 连接会话,管理认证和通道创建。Channel:会话中打开的通道类型,如ChannelShell(交互式 Shell)、ChannelExec(单命令执行)、ChannelSftp(文件传输)。
⚙️ 二、核心功能与技术特性
-
认证方式
JSch 支持 4 种认证机制,实际开发中常见的是前两种:- 密码认证:
session.setPassword("password") - 公私钥认证:
jsch.addIdentity("~/.ssh/id_rsa")(推荐免密登录) - 键盘交互式(
keyboard-interactive) - GSSAPI(
gss-api-with-mic,适用于企业级域认证)。
- 密码认证:
-
文件传输(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(追加内容)。
-
端口转发
- 本地转发(访问本地端口 → 转发到远程服务):
session.setPortForwardingL(8080, "remote-host", 3306); // 本地 8080 → 远程 MySQL - 远程转发(暴露本地服务到远程端口):
session.setPortForwardingR(9090, "localhost", 3000); // 远程 9090 → 本地 Web 服务 - 动态转发(SOCKS 代理):
session.setPortForwardingD(1080); // 创建 SOCKS5 代理
- 本地转发(访问本地端口 → 转发到远程服务):
🛠️ 三、基础使用步骤(附代码)
-
添加 Maven 依赖
<dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.55</version> </dependency> -
建立连接并执行命令
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(); } } }
🔐 四、安全实践与关键配置
-
主机密钥检查(
StrictHostKeyChecking)ask(默认):首次连接提示用户确认密钥(不适用于自动化)。no:自动接受新密钥(仅测试环境使用)。yes:严格匹配,密钥不匹配则拒绝连接(生产环境推荐)。
📌 建议:预置已知主机密钥到
~/.ssh/known_hosts,避免关闭检查。 -
强化安全性的措施
- 禁用密码登录:在
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 避免频繁重建连接。

浙公网安备 33010602011771号