关于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开发手册(嵩山版)》最新公布,速速下载!

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理