乐趣区

关于java:Java-打造一款-SSH-客户端已开源~

起源:https://blog.csdn.net/NoCortY…

前言

最近因为我的项目需要,我的项目中须要实现一个 WebSSH 连贯终端的性能,因为本人第一次做这类型性能,所以首先上了 GitHub 找了找有没有现成的轮子能够拿来间接用,过后看到了很多这方面的我的项目,例如:GateOne、webssh、shellinabox 等。

这些我的项目都能够很好地实现 webssh 的性能,然而最终并没有采纳,起因是在于这些底层大都是 python 写的,须要依赖很多文件,本人用的时候能够应用这种计划,快捷省事,然而做到我的项目中供用户应用时,总不能要求用户做到服务器中必须蕴含这些底层依赖,这显然不太正当,所以我决定本人入手写一个 WebSSH 的性能,并且作为一个独立的我的项目开源进去。(文末附我的项目开源地址)

技术选型

因为 webssh 须要实时数据交互,所以会选用长连贯的 WebSocket,为了开发的不便,框架选用 SpringBoot,另外还本人理解了 Java 用户连贯 ssh 的 jsch 和实现前端 shell 页面的 xterm.js. 所以,最终的技术选型就是 SpringBoot+Websocket+jsch+xterm.js。

导入依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <!-- Web 相干 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- jsch 反对 -->
    <dependency>
        <groupId>com.jcraft</groupId>
        <artifactId>jsch</artifactId>
        <version>0.1.54</version>
    </dependency>
    <!-- WebSocket 反对 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- 文件上传解析器 -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.4</version>
    </dependency>
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
</dependencies>

一个简略的 xterm 案例

因为 xterm 是一个冷门技术,所以很多同学并没有这方面的常识撑持,我也是为了实现这个性能所以长期学的,所以在这给大家介绍一下。

xterm.js 是一个基于 WebSocket 的容器,它能够帮忙咱们在前端实现命令行的款式。就像是咱们平时再用 SecureCRT 或者 XShell 连贯服务器时一样。上面是官网上的入门案例:

<!doctype html>
 <html>
  <head>
    <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
    <script src="node_modules/xterm/lib/xterm.js"></script>
  </head>
  <body>
    <div id="terminal"></div>
    <script>
      var term = new Terminal();
      term.open(document.getElementById('terminal'));
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $')
</script>
  </body>
 </html>

最终测试,页面就是上面这个样子:能够看到页面曾经呈现了相似与 shell 的款式,那就依据这个持续深刻,实现一个 webssh。

后端实现

因为 xterm 只有只是实现了前端的款式,并不能真正地实现与服务器交互,与服务器交互次要还是靠咱们 Java 后端来进行管制的,所以咱们从后端开始,应用 jsch+websocket 实现这部分内容。

WebSocket 配置

因为音讯实时推送到前端须要用到 WebSocket,不理解 WebSocket 的同学能够先去自行理解一下,这里就不过多介绍了,咱们间接开始进行 WebSocket 的配置。

/**
* @Description: websocket 配置
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Configuration
@EnableWebSocket
public class WebSSHWebSocketConfig implements WebSocketConfigurer{
    @Autowired
    WebSSHWebSocketHandler webSSHWebSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        //socket 通道
        // 指定处理器和门路,并设置跨域
        webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
                .addInterceptors(new WebSocketInterceptor())
                .setAllowedOrigins("*");
    }
}

处理器 (Handler) 和拦截器 (Interceptor) 的实现

方才咱们实现了 WebSocket 的配置,并指定了一个处理器和拦截器。所以接下来就是处理器和拦截器的实现。拦截器:

public class WebSocketInterceptor implements HandshakeInterceptor {
    /**
     * @Description: Handler 解决前调用
     * @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
     * @return: boolean
     * @Author: NoCortY
     * @Date: 2020/3/1
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {if (serverHttpRequest instanceof ServletServerHttpRequest) {ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
            // 生成一个 UUID,这里因为是独立的我的项目,没有用户模块,所以能够用随机的 UUID
            // 然而如果要集成到本人的我的项目中,须要将其改为本人辨认用户的标识
            String uuid = UUID.randomUUID().toString().replace("-","");
            // 将 uuid 放到 websocketsession 中
            map.put(ConstantPool.USER_UUID_KEY, uuid);
            return true;
        } else {return false;}
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {}}

处理器:

/**
* @Description: WebSSH 的 WebSocket 处理器
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Component
public class WebSSHWebSocketHandler implements WebSocketHandler{
    @Autowired
    private WebSSHService webSSHService;
    private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);

    /**
     * @Description: 用户连贯上 WebSocket 的回调
     * @Param: [webSocketSession]
     * @return: void
     * @Author: Object
     * @Date: 2020/3/8
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {logger.info("用户:{}, 连贯 WebSSH", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY));
        // 调用初始化连贯
        webSSHService.initConnection(webSocketSession);
    }

    /**
     * @Description: 收到音讯的回调
     * @Param: [webSocketSession, webSocketMessage]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/8
     */
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {if (webSocketMessage instanceof TextMessage) {logger.info("用户:{}, 发送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
            // 调用 service 接管音讯
            webSSHService.recvHandle(((TextMessage) webSocketMessage).getPayload(), webSocketSession);
        } else if (webSocketMessage instanceof BinaryMessage) {} else if (webSocketMessage instanceof PongMessage) { } else {System.out.println("Unexpected WebSocket message type:" + webSocketMessage);
        }
    }

    /**
     * @Description: 呈现谬误的回调
     * @Param: [webSocketSession, throwable]
     * @return: void
     * @Author: Object
     * @Date: 2020/3/8
     */
    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {logger.error("数据传输谬误");
    }

    /**
     * @Description: 连贯敞开的回调
     * @Param: [webSocketSession, closeStatus]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/8
     */
    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {logger.info("用户:{}断开 webssh 连贯", String.valueOf(webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY)));
        // 调用 service 敞开连贯
        webSSHService.close(webSocketSession);
    }

    @Override
    public boolean supportsPartialMessages() {return false;}
}

须要留神的是,我在拦截器中退出的用户标识是应用了随机的 UUID,这是因为作为一个独立的 websocket 我的项目,没有用户模块,如果须要将这个我的项目集成到本人的我的项目中,须要批改这部分代码,将其改为本人我的项目中辨认一个用户所用的用户标识。

WebSSH 的业务逻辑实现(外围)

方才咱们实现了 websocket 的配置,都是一些死代码,实现了接口再依据本身需要即可实现,当初咱们将进行后端次要业务逻辑的实现,在实现这个逻辑之前,咱们先来想想,WebSSH,咱们次要想要出现一个什么成果。我这里做了一个总结:

1. 首先咱们得先连贯上终端(初始化连贯)2. 其次咱们的服务端须要解决来自前端的音讯(接管并解决前端音讯)3. 咱们须要将终端返回的音讯回写到前端(数据回写前端)4. 敞开连贯

依据这四个需要,咱们先定义一个接口,这样能够让需要明了起来。

/**
 * @Description: WebSSH 的业务逻辑
 * @Author: NoCortY
 * @Date: 2020/3/7
 */
public interface WebSSHService {
    /**
     * @Description: 初始化 ssh 连贯
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void initConnection(WebSocketSession session);

    /**
     * @Description: 解决客户段发的数据
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void recvHandle(String buffer, WebSocketSession session);

    /**
     * @Description: 数据写回前端 for websocket
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;

    /**
     * @Description: 敞开连贯
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void close(WebSocketSession session);
}

当初咱们能够依据这个接口去实现咱们定义的性能了。1. 初始化连贯 因为咱们的底层是依赖 jsch 实现的,所以这里是须要应用 jsch 去建设连贯的。而所谓初始化连贯,实际上就是将咱们所须要的连贯信息,保留在一个 Map 中,这里并不进行任何的实在连贯操作。为什么这里不间接进行连贯?因为这里前端只是连贯上了 WebSocket,然而咱们还须要前端给咱们发来 linux 终端的用户名和明码,没有这些信息,咱们是无奈进行连贯的。

public void initConnection(WebSocketSession session) {JSch jSch = new JSch();
        SSHConnectInfo sshConnectInfo = new SSHConnectInfo();
        sshConnectInfo.setjSch(jSch);
        sshConnectInfo.setWebSocketSession(session);
        String uuid = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
        // 将这个 ssh 连贯信息放入 map 中
        sshMap.put(uuid, sshConnectInfo);
}

2. 解决客户端发送的数据 在这一步骤中,咱们会分为两个分支。第一个分支:如果客户端发来的是终端的用户名和明码等信息,那么咱们进行终端的连贯。第二个分支:如果客户端发来的是操作终端的命令,那么咱们就间接转发到终端并且获取终端的执行后果。具体代码实现:

public void recvHandle(String buffer, WebSocketSession session) {ObjectMapper objectMapper = new ObjectMapper();
        WebSSHData webSSHData = null;
        try {
            // 转换前端发送的 JSON
            webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
        } catch (IOException e) {logger.error("Json 转换异样");
            logger.error("异样信息:{}", e.getMessage());
            return;
        }
    // 获取方才设置的随机的 uuid
        String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
        if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
            // 如果是连贯申请
            // 找到方才存储的 ssh 连贯对象
            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
            // 启动线程异步解决
            WebSSHData finalWebSSHData = webSSHData;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 连贯到终端
                        connectToSSH(sshConnectInfo, finalWebSSHData, session);
                    } catch (JSchException | IOException e) {logger.error("webssh 连贯异样");
                        logger.error("异样信息:{}", e.getMessage());
                        close(session);
                    }
                }
            });
        } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
            // 如果是发送命令的申请
            String command = webSSHData.getCommand();
            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
            if (sshConnectInfo != null) {
                try {
                    // 发送命令到终端
                    transToSSH(sshConnectInfo.getChannel(), command);
                } catch (IOException e) {logger.error("webssh 连贯异样");
                    logger.error("异样信息:{}", e.getMessage());
                    close(session);
                }
            }
        } else {logger.error("不反对的操作");
            close(session);
        }
}

3. 数据通过 websocket 发送到前端

public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {session.sendMessage(new TextMessage(buffer));
}

4. 敞开连贯

public void close(WebSocketSession session) {
    // 获取随机生成的 uuid
        String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
        SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
        if (sshConnectInfo != null) {
            // 断开连接
            if (sshConnectInfo.getChannel() != null) sshConnectInfo.getChannel().disconnect();
            //map 中移除该 ssh 连贯信息
            sshMap.remove(userId);
        }
}

至此,咱们的整个后端实现就完结了,因为篇幅无限,这里将一些操作封装成了办法,就不做过多展现了,重点讲逻辑实现的思路吧。接下来咱们将进行前端的实现。

前端实现

前端工作次要分为这么几个步骤:

  1. 页面的实现
  2. 连贯 WebSocket 并实现数据的接管并回写
  3. 数据的发送

所以咱们一步一步来实现它。

页面实现

页面的实现很简略,咱们只不过须要在一整个屏幕上都显示终端那种大黑屏幕,所以咱们并不必写什么款式,只须要创立一个 div,之后将 terminal 实例通过 xterm 放到这个 div 中,就能够实现了。

<!doctype html>
<html>
<head>
    <title>WebSSH</title>
    <link rel="stylesheet" href="../css/xterm.css" />
</head>
<body>
<div id="terminal" style="width: 100%;height: 100%"></div>

<script src="../lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
<script src="../js/xterm.js" charset="utf-8"></script>
<script src="../js/webssh.js" charset="utf-8"></script>
<script src="../js/base64.js" charset="utf-8"></script>
</body>
</html>

连贯 WebSocket 并实现数据的发送、接管、回写

openTerminal( {
    // 这里的内容能够写死,然而要整合到我的项目中时,须要通过参数的形式传入,能够动静连贯某个终端。operate:'connect',
        host: 'ip 地址',
        port: '端口号',
        username: '用户名',
        password: '明码'
    });
    function openTerminal(options){var client = new WSSHClient();
        var term = new Terminal({
            cols: 97,
            rows: 37,
            cursorBlink: true, // 光标闪动
            cursorStyle: "block", // 光标款式  null | 'block' | 'underline' | 'bar'
            scrollback: 800, // 回滚
            tabStopWidth: 8, // 制表宽度
            screenKeys: true
        });

        term.on('data', function (data) {
            // 键盘输入时的回调函数
            client.sendClientData(data);
        });
        term.open(document.getElementById('terminal'));
        // 在页面上显示连贯中...
        term.write('Connecting...');
        // 执行连贯操作
        client.connect({onError: function (error) {
                // 连贯失败回调
                term.write('Error:' + error + '\r\n');
            },
            onConnect: function () {
                // 连贯胜利回调
                client.sendInitData(options);
            },
            onClose: function () {
                // 连贯敞开回调
                term.write("\rconnection closed");
            },
            onData: function (data) {
                // 收到数据时回调
                term.write(data);
            }
        });
    }

成果展现

连贯

连贯胜利

命令操作

ls 命令:

vim 编辑器:

top 命令:

结语

这样咱们就实现了一个 webssh 我的项目的实现,没有依赖其它任何的组件,后端齐全应用 Java 实现,因为用了 SpringBoot,非常容易部署。

然而,咱们还能够对这个我的项目进行扩大,比方新增上传或下载文件,就像 Xftp 一样,能够很不便地拖拽式上传下载文件。

Github 我的项目开源地址:https://github.com/NoCortY/We…

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 别在再满屏的 if/ else 了,试试策略模式,真香!!

3. 卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅公布,光明模式太炸了!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版