简介
Jsch 是一个 Java 实现的 SSH2 协议库,它允许 Java 应用程序通过 SSH 安全地连接到远程服务器,执行命令,并传输文件。Jsch 提供了一种简单而强大的方式来实现远程连接和操作,适用于需要在 Java 应用程序中与远程服务器进行通信的场景。
背景
积累知识。
教程
说明:该工具类是在Hutool工具类JschUtil的基础上进行编写的,主要为了扩展自身需求:实现服务器终端功能,这里代码已将WebSocket抽出去了(使用技术:WebSocket + Xterm)
1、pom依赖
<!--Hutool核心工具-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.7.22</version>
</dependency>
<!--代码简化工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!--ssh连接工具-->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.51</version>
</dependency>
2、Jsch通道类型
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @Author ClancyLv
* @Date 2024/8/14 11:10
* @Description 枚举类--jsch通道类型
*/
@Getter
@AllArgsConstructor
public enum JschChannelType {
/** Session */
SESSION("session"),
/** shell */
SHELL("shell"),
/** exec */
EXEC("exec"),
/** x11 */
X11("x11"),
/** agent forwarding */
AGENT_FORWARDING("auth-agent@openssh.com"),
/** direct tcpip */
DIRECT_TCPIP("direct-tcpip"),
/** forwarded tcpip */
FORWARDED_TCPIP("forwarded-tcpip"),
/** sftp */
SFTP("sftp"),
/** subsystem */
SUBSYSTEM("subsystem");
/** channel值 */
private final String value;
}
3、SSH会话池
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import java.util.HashMap;
import java.util.Map;
/**
* @Author ClancyLv
* @Date 2024/8/14 10:42
* @Description SSH会话池
*/
public enum JschSessionPool {
INSTANCE;
/**
* SSH会话池,key:suer@host:port,value:Session对象
*/
private final HashMap<String, Session> cache = new HashMap<>();
/**
* 获得一个SSH跳板机会话,重用已经使用的会话
*
* @param sshHost 跳板机主机
* @param sshPort 跳板机端口
* @param sshUser 跳板机用户名
* @param sshPass 跳板机密码
* @return SSH会话
*/
public Session getSession(String sshHost, int sshPort, String sshUser, String sshPass) throws JSchException {
final String key = String.format("%s@%s:%s", sshUser, sshHost, sshPort);
Session session = this.cache.get(key);
if (session == null || !session.isConnected()) {
return Jsch.openSession(sshHost, sshPort, sshUser, sshPass);
}
return session;
}
/**
* 获得一个SSH跳板机会话,重用已经使用的会话
*
* @param sshHost 跳板机主机
* @param sshPort 跳板机端口
* @param sshUser 跳板机用户名
* @param prvkey 跳板机私钥路径
* @param passphrase 跳板机私钥密码
* @return SSH会话
*/
public Session getSession(String sshHost, int sshPort, String sshUser, String prvkey, byte[] passphrase) throws JSchException {
final String key = String.format("%s@%s:%s", sshUser, sshHost, sshPort);
Session session = this.cache.get(key);
if (session == null || !session.isConnected()) {
return Jsch.openSession(sshHost, sshPort, sshUser, prvkey, passphrase);
}
return session;
}
/**
* 获取Session
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
*/
public Session get(String sshHost, int sshPort, String sshUser) {
String key = String.format("%s@%s:%s", sshUser, sshHost, sshPort);
return this.get(key);
}
/**
* 获取Session
*
* @param key 键
*/
public Session get(String key) {
return this.cache.get(key);
}
/**
* 加入Session
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
* @param session Session
*/
public void put(String sshHost, int sshPort, String sshUser, Session session) {
String key = String.format("%s@%s:%s", sshUser, sshHost, sshPort);
this.put(key, session);
}
/**
* 加入Session
*
* @param key 键
* @param session Session
*/
public void put(String key, Session session) {
this.cache.put(key, session);
}
/**
* 关闭SSH连接会话
*
* @param key 主机,格式为user@host:port
*/
public void close(String key) {
Session session = this.cache.get(key);
if (session != null && session.isConnected()) {
session.disconnect();
}
this.cache.remove(key);
}
/**
* 移除指定Session
*
* @param session Session会话
* @since 4.1.15
*/
public void close(Session session) {
if (session != null) {
for (Map.Entry<String, Session> entry : this.cache.entrySet()) {
if (session.equals(entry.getValue())) {
this.cache.remove(entry.getKey());
break;
}
}
if (session.isConnected()) {
session.disconnect();
}
}
}
/**
* 关闭所有SSH连接会话
*/
public void closeAll() {
Session session;
for (Map.Entry<String, Session> entry : this.cache.entrySet()) {
session = entry.getValue();
if (session != null && session.isConnected()) {
session.disconnect();
}
}
cache.clear();
}
}
4、Jsch工具类
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @Author ClancyLv
* @Date 2024/8/14 10:35
* @Description 工具类--Jsch工具类
*/
public class Jsch {
/**
* 获得一个SSH会话,重用已经使用的会话
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
* @param sshPass 密码
* @return SSH会话
*/
public static Session getSession(String sshHost, int sshPort, String sshUser, String sshPass) throws JSchException {
return JschSessionPool.INSTANCE.getSession(sshHost, sshPort, sshUser, sshPass);
}
/**
* 获得一个SSH会话,重用已经使用的会话
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
* @param privateKeyPath 私钥路径
* @param passphrase 私钥密码
* @return SSH会话
*/
public static Session getSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase) throws JSchException {
return JschSessionPool.INSTANCE.getSession(sshHost, sshPort, sshUser, privateKeyPath, passphrase);
}
/**
* 打开一个新的SSH会话
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
* @param sshPass 密码
* @return SSH会话
*/
public static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass) throws JSchException {
return openSession(sshHost, sshPort, sshUser, sshPass, 0);
}
/**
* 打开一个新的SSH会话
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
* @param sshPass 密码
* @param timeout Socket连接超时时长,单位毫秒
* @return SSH会话
* @since 5.3.3
*/
public static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass, int timeout) throws JSchException {
final Session session = createSession(sshHost, sshPort, sshUser, sshPass);
try {
session.connect(timeout);
} catch (JSchException e) {
throw new JSchException("认证失败,请检查配置信息:" + e.getMessage());
}
return session;
}
/**
* 打开一个新的SSH会话
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名
* @param privateKeyPath 私钥的路径
* @param passphrase 私钥文件的密码,可以为null
* @return SSH会话
*/
public static Session openSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase) throws JSchException {
final Session session = createSession(sshHost, sshPort, sshUser, privateKeyPath, passphrase);
try {
session.connect();
} catch (JSchException e) {
throw new JSchException("认证失败,请检查配置信息:" + e.getMessage());
}
return session;
}
/**
* 新建一个新的SSH会话,此方法并不打开会话(既不调用connect方法)
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名,如果为null,默认root
* @param sshPass 密码
* @return SSH会话
* @since 4.5.2
*/
public static Session createSession(String sshHost, int sshPort, String sshUser, String sshPass) throws JSchException {
final JSch jsch = new JSch();
final Session session = createSession(jsch, sshHost, sshPort, sshUser);
if (StrUtil.isNotBlank(sshPass)) {
session.setPassword(sshPass);
}
return session;
}
/**
* 新建一个新的SSH会话,此方法并不打开会话(既不调用connect方法)
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名,如果为null,默认root
* @param privateKeyPath 私钥的路径
* @param passphrase 私钥文件的密码,可以为null
* @return SSH会话
* @since 5.0.0
*/
public static Session createSession(String sshHost, int sshPort, String sshUser, String privateKeyPath, byte[] passphrase) throws JSchException {
if (StrUtil.isBlank(privateKeyPath)) {
throw new IllegalArgumentException("私钥的路径不能为空!");
}
final JSch jsch = new JSch();
try {
jsch.addIdentity(privateKeyPath, passphrase);
} catch (JSchException e) {
throw new JSchException(e.getMessage());
}
return createSession(jsch, sshHost, sshPort, sshUser);
}
/**
* 创建一个SSH会话
*
* @param jsch {@link JSch}
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名,如果为null,默认root
* @return {@link Session}
* @since 5.0.3
*/
public static Session createSession(JSch jsch, String sshHost, int sshPort, String sshUser) throws JSchException {
if (StrUtil.isBlank(sshHost)) {
throw new IllegalArgumentException("主机地址不能为空!");
}
if (sshPort == 0) {
throw new IllegalArgumentException("端口号不能为0!");
}
// 默认root用户
if (StrUtil.isEmpty(sshUser)) {
sshUser = "root";
}
if (null == jsch) {
jsch = new JSch();
}
Session session;
try {
session = jsch.getSession(sshUser, sshHost, sshPort);
} catch (JSchException e) {
throw new JSchException(e.getMessage());
}
// 设置第一次登录的时候提示,可选值:(ask | yes | no)
session.setConfig("StrictHostKeyChecking", "no");
// 缓存会话
JschSessionPool.INSTANCE.put(sshHost, sshPort, sshUser, session);
return session;
}
/**
* 绑定端口到本地。 一个会话可绑定多个端口
*
* @param session 需要绑定端口的SSH会话
* @param remoteHost 远程主机
* @param remotePort 远程端口
* @param localPort 本地端口
* @return 成功与否
* @throws JSchException 端口绑定失败异常
*/
public static boolean bindPort(Session session, String remoteHost, int remotePort, int localPort) throws JSchException {
return bindPort(session, remoteHost, remotePort, "127.0.0.1", localPort);
}
/**
* 绑定端口到本地。 一个会话可绑定多个端口
*
* @param session 需要绑定端口的SSH会话
* @param remoteHost 远程主机
* @param remotePort 远程端口
* @param localHost 本地主机
* @param localPort 本地端口
* @return 成功与否
* @throws JSchException 端口绑定失败异常
* @since 5.7.8
*/
public static boolean bindPort(Session session, String remoteHost, int remotePort, String localHost, int localPort) throws JSchException {
if (session != null && session.isConnected()) {
try {
session.setPortForwardingL(localHost, localPort, remoteHost, remotePort);
} catch (JSchException e) {
throw new JSchException(String.format("From [%s:%s] mapping to [%s:%s] error!", remoteHost, remotePort, localHost, localPort));
}
return true;
}
return false;
}
/**
* 绑定ssh服务端的serverPort端口, 到host主机的port端口上. <br>
* 即数据从ssh服务端的serverPort端口, 流经ssh客户端, 达到host:port上.
*
* @param session 与ssh服务端建立的会话
* @param bindPort ssh服务端上要被绑定的端口
* @param host 转发到的host
* @param port host上的端口
* @return 成功与否
* @throws JSchException 端口绑定失败异常
* @since 5.4.2
*/
public static boolean bindRemotePort(Session session, int bindPort, String host, int port) throws JSchException {
if (session != null && session.isConnected()) {
try {
session.setPortForwardingR(bindPort, host, port);
} catch (JSchException e) {
throw new JSchException(String.format("From [%s] mapping to [%s] error!", bindPort, port));
}
return true;
}
return false;
}
/**
* 解除端口映射
*
* @param session 需要解除端口映射的SSH会话
* @param localPort 需要解除的本地端口
* @return 解除成功与否
*/
public static boolean unBindPort(Session session, int localPort) throws JSchException {
try {
session.delPortForwardingL(localPort);
} catch (JSchException e) {
throw new JSchException(e.getMessage());
}
return true;
}
/**
* 打开SFTP连接
*
* @param session Session会话
* @return {@link ChannelSftp}
* @since 4.0.3
*/
public static ChannelSftp openSftp(Session session) throws JSchException {
return openSftp(session, 0);
}
/**
* 打开SFTP连接
*
* @param session Session会话
* @param timeout 连接超时时长,单位毫秒
* @return {@link ChannelSftp}
* @since 5.3.3
*/
public static ChannelSftp openSftp(Session session, int timeout) throws JSchException {
return (ChannelSftp) openChannel(session, JschChannelType.SFTP, timeout);
}
/**
* 打开Shell连接
*
* @param session Session会话
* @return {@link ChannelShell}
* @since 4.0.3
*/
public static ChannelShell openShell(Session session) throws JSchException {
return (ChannelShell) openChannel(session, JschChannelType.SHELL);
}
/**
* 打开Channel连接
*
* @param session Session会话
* @param channelType 通道类型,可以是shell或sftp等,见{@link JschChannelType}
* @return {@link Channel}
* @since 4.5.2
*/
public static Channel openChannel(Session session, JschChannelType channelType) throws JSchException {
return openChannel(session, channelType, 0);
}
/**
* 打开Channel连接
*
* @param session Session会话
* @param channelType 通道类型,可以是shell或sftp等,见{@link JschChannelType}
* @param timeout 连接超时时长,单位毫秒
* @return {@link Channel}
* @since 5.3.3
*/
public static Channel openChannel(Session session, JschChannelType channelType, int timeout) throws JSchException {
final Channel channel = createChannel(session, channelType);
try {
channel.connect(Math.max(timeout, 0));
} catch (JSchException e) {
throw new JSchException(e.getMessage());
}
return channel;
}
/**
* 创建Channel连接
*
* @param session Session会话
* @param channelType 通道类型,可以是shell或sftp等,见{@link JschChannelType}
* @return {@link Channel}
* @since 4.5.2
*/
public static Channel createChannel(Session session, JschChannelType channelType) throws JSchException {
Channel channel;
try {
if (!session.isConnected()) {
session.connect();
}
channel = session.openChannel(channelType.getValue());
} catch (JSchException e) {
throw new JSchException("认证失败,请检查配置信息:" + e.getMessage());
}
return channel;
}
/**
* 执行Shell命令
*
* @param session Session会话
* @param cmd 命令
* @return {@link ChannelExec}
* @since 4.0.3
*/
public static String exec(Session session, String cmd) throws JSchException, IOException {
return exec(session, cmd, System.err);
}
/**
* 执行Shell命令(使用EXEC方式)
* <p>
* 此方法单次发送一个命令到服务端,不读取环境变量,执行结束后自动关闭channel,不会产生阻塞。
* </p>
*
* @param session Session会话
* @param cmd 命令
* @param errStream 错误信息输出到的位置
* @return 执行结果内容
* @since 4.3.1
*/
public static String exec(Session session, String cmd, OutputStream errStream) throws IOException, JSchException {
return exec(session, new ArrayList<String>(){{ add(cmd); }}, errStream);
}
/**
* 执行Shell命令
*
* @param session Session会话
* @param cmdList 命令集合
* @return {@link ChannelExec}
* @since 4.0.3
*/
public static String exec(Session session, List<String> cmdList) throws JSchException, IOException {
return exec(session, cmdList, System.err);
}
/**
* 执行Shell命令(使用EXEC方式)
* <p>
* 此方法单次发送一个命令到服务端,不读取环境变量,执行结束后自动关闭channel,不会产生阻塞。
* </p>
*
* @param session Session会话
* @param cmdList 命令集合
* @param errStream 错误信息输出到的位置
* @return 执行结果内容
* @since 4.3.1
*/
public static String exec(Session session, List<String> cmdList, OutputStream errStream) throws JSchException, IOException {
final ChannelExec channel = (ChannelExec) createChannel(session, JschChannelType.EXEC);
// 处理命令列表
String handleCmd = String.join(" && ", cmdList);
channel.setCommand(handleCmd.getBytes(StandardCharsets.UTF_8));
channel.setInputStream(null);
channel.setErrStream(errStream);
InputStream in = null;
BufferedReader reader = null;
try {
channel.connect();
in = channel.getInputStream();
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
return stringBuilder.toString();
} catch (IOException e) {
throw new IOException("文件读取异常:" + e.getMessage());
} catch (JSchException e) {
throw new JSchException("认证失败,请检查配置信息:" + e.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
}
}
close(channel);
}
}
/**
* 此方法需要封装成异步调用,避免阻塞
* 打开终端并监听,需要自己存储ChannelShell通道,用于正确读取和发送消息(用于实现终端交互)
* while中循环等待读取消息,可根据自身需求去推送消息,一般使用WebSocket进行推送
* @param shell shell通道
* @throws IOException IO异常
*/
public static void openTerminal(ChannelShell shell) throws IOException {
InputStream in = null;
try {
in = shell.getInputStream();
//循环读取
byte[] buffer = new byte[8192];
int i;
//如果没有数据来,线程会一直阻塞在这个地方等待数据。
while ((i = in.read(buffer)) != -1) {
System.out.println(new String(Arrays.copyOfRange(buffer, 0, i), StandardCharsets.UTF_8));
}
} catch (IOException e) {
throw new IOException(e.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {
}
}
}
}
/**
* 在终端执行命令,配合上面方法openTerminal使用
* @param shell 打开终端时候保存的ChannelShell
* @param cmd 要执行的命令
* @throws IOException IO异常
* @throws JSchException Jsch异常
*/
public static void execByTerminal(ChannelShell shell, String cmd) throws IOException, JSchException {
if (shell != null) {
try {
OutputStream out = shell.getOutputStream();
out.write(cmd.getBytes());
out.flush();
} catch (IOException e) {
throw new IOException(e.getMessage());
}
}
}
/**
* 关闭SSH连接会话
*
* @param session SSH会话
*/
public static void close(Session session) {
JschSessionPool.INSTANCE.close(session);
}
/**
* 关闭会话通道
*
* @param channel 会话通道
* @since 4.0.3
*/
public static void close(Channel channel) {
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
}
/**
* 关闭SSH连接会话
*
* @param key 主机,格式为user@host:port
*/
public static void close(String key) {
JschSessionPool.INSTANCE.close(key);
}
/**
* 关闭所有SSH连接会话
*/
public static void closeAll() {
JschSessionPool.INSTANCE.closeAll();
}
}