Java—SSH连接工具库Jsch

简介

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();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值