Java打造一款SSH客户端,已开源!

最近由于项目需求,项目中需要实现一个WebSSH连接终端的功能,由于自己第一次做这类型功能,所以首先上了GitHub找了找有没有现成的轮子可以拿来直接用,当时看到了很多这方面的项目,例如:GateOne、webssh、shellinabox等,这些项目都可以很好地实现webssh的功能,但是最终并没有采用。

原因是在于这些底层大都是python写的,需要依赖很多文件,自己用的时候可以使用这种方案,快捷省事,但是做到项目中供用户使用时,总不能要求用户做到服务器中必须包含这些底层依赖,这显然不太合理,所以我决定自己动手写一个WebSSH的功能,并且作为一个独立的项目开源出来。

github项目开源地址:https://github.com/NoCortY/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>

最终测试,页面就是下面这个样子:

xterm入门

可以看到页面已经出现了类似与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{@AutowiredWebSSHWebSocketHandler webSSHWebSocketHandler;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {//socket通道//指定处理器和路径,并设置跨域webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*");}
}

处理器和拦截器的实现

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

拦截器

public class WebSocketInterceptor implements HandshakeInterceptor {/*** @Description: Handler处理前调用* @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]* @return: boolean* @Author: NoCortY* @Date: 2020/3/1*/@Overridepublic 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;}}@Overridepublic 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{@Autowiredprivate WebSSHService webSSHService;private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);/*** @Description: 用户连接上WebSocket的回调* @Param: [webSocketSession]* @return: void* @Author: Object* @Date: 2020/3/8*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {logger.error("数据传输错误");}/*** @Description: 连接关闭的回调* @Param: [webSocketSession, closeStatus]* @return: void* @Author: NoCortY* @Date: 2020/3/8*/@Overridepublic void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {logger.info("用户:{}断开webssh连接", String.valueOf(webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY)));//调用service关闭连接webSSHService.close(webSocketSession);}@Overridepublic 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);
}
123456789101112131415161718192021222324252627282930313233343536373839404142

现在我们可以根据这个接口去实现我们定义的功能了。

  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);
}
  1. 处理客户端发送的数据

    在这一步骤中,我们会分为两个分支。

    第一个分支:如果客户端发来的是终端的用户名和密码等信息,那么我们进行终端的连接。

    第二个分支:如果客户端发来的是操作终端的命令,那么我们就直接转发到终端并且获取终端的执行结果。

    具体代码实现:

public void recvHandle(String buffer, WebSocketSession session) {ObjectMapper objectMapper = new ObjectMapper();WebSSHData webSSHData = null;try {//转换前端发送的JSONwebSSHData = objectMapper.readValue(buffer, WebSSHData.class);} catch (IOException e) {logger.error("Json转换异常");logger.error("异常信息:{}", e.getMessage());return;}//获取刚才设置的随机的uuidString 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() {@Overridepublic 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);}
}
  1. 数据通过websocket发送到前端

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

public void close(WebSocketSession session) {//获取随机生成的uuidString 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命令:

ls命令

vim编辑器:

vim编辑器

top命令:

top命令

结语

这样我们就完成了一个webssh项目的实现,没有依赖其它任何的组件,后端完全使用Java实现,由于用了SpringBoot,非常容易部署。

但是,我们还可以对这个项目进行扩展,比如新增上传或下载文件,就像Xftp一样,可以很方便地拖拽式上传下载文件。

这个项目之后我会持续更新,上述功能也会慢慢实现,Github:https://github.com/NoCortY/WebSSH


往期推荐

Spring 事务失效的 8 大场景,面试官直呼666...


学到了!MySQL 8 新增的「隐藏索引」真不错


这个 bug 让我更加理解 Spring 单例了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544954.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

html中可以自定义属性,,,妈的竟然才知道..

html中可以自定义属性,,,妈的竟然才知道.. <input userinfo"没见过帅哥呀" />

Js实现动态插入删除文本框

自己做了个Js插入文本框的例子&#xff0c;扔上别忘了。 <html><head><title>Untitled Document</title><meta http-equiv"Content-Type" content"text/html; charsetutf-8"><script language"javascript"&g…

ruby 数组元素替换_从Ruby中的集合中删除并替换元素

ruby 数组元素替换Ruby has various specific methods to fulfil specific tasks. At several places, you may need to replace or delete an element from an instance of set class provided that there are certain elements present in the Set. You can perform deletion…

我去,这几个Linux指令太装B了|动图展示

1. sl先看一下呼啸而过的火车&#xff1b;安装指令如下&#xff1b;sduo apt-get install sl执行结果如下&#xff1a;2. htop图形化Linux系统性能监测工具&#xff0c;屌不屌:安装指令如下:sduo apt-get install htop执行结果如下&#xff1b;3. gcp以前用cp复制文件总是看不懂…

书店POS机--细化迭代2--测试

2019独角兽企业重金招聘Python工程师标准>>> (1) 开始一次新的销售&#xff0c;点击书店POS系统的销售&#xff1a; (2) 进入销售模块之后的界面如下&#xff1a; (3)逐条录入商品条目(根据商品编号)&#xff0c;并修改数量。确认无误之后点击“确认”按钮&#x…

XML文件的写入和读取(解析)基于DOM4J工具

这两天做了个天气的小应用&#xff0c;需要用到百度的天气api&#xff0c;获取到的信息是一个xml文档。 所以就从网上查了一下相关的知识&#xff0c;就是关于怎么去解析出来xml文件的信息。 先放一个我自己写的例子&#xff0c;加了点注释&#xff0c;贴这里吧。 package cn…

Google Guava,牛逼的脚手架

01、前世今生你好呀&#xff0c;我是 Guava。1995 年的时候&#xff0c;我的“公明”哥哥——Java 出生了。经过 20 年的发展&#xff0c;他已经成为世界上最流行的编程语言了&#xff0c;请允许我有失公允的把“之一”给去了。虽然他时常遭受着各种各样的吐槽&#xff0c;但他…

ruby 将字符转数字计算_Ruby程序计算一个数字中的位数

ruby 将字符转数字计算计算位数 (Counting the number of digits) Ruby does not provide you any predefined direct method through which you can find the number of digits in a number. Though one method can be implemented by converting the number into a string an…

python_L2_operator

1. i/j – division – if both are ints, result is int, represen9ng quo9ent without remainder e.g. 3/2 1 3.0/2 1.5  but   -3/2 -2 2. --2 >>> 2 3. a * 2 >>> aa 4 . abc[-1] returns c 5. world[:-1] returns worl转载于:https://www.cnbl…

Android Call requires API level 11 (current min is 8)的解决方案

【错误描述】 在用Eclipse开发过程中&#xff0c;为了兼容Android2.2和4.0以上版本&#xff0c;我在使用Notification类时做了2个版本的代码&#xff0c;代码根据系统版本不同执行相应模块&#xff0c;结果&#xff0c;等我输完代码&#xff0c;发现系统提示了一个这么的错误。…

阿里巴巴Druid,轻松实现MySQL数据库加密!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;为什么要加密&#xff1f;现在的开发习惯&#xff0c;无论是公司的项目还是个人的项目&#xff0c;都会选择将源码上传到 Gi…

计算机图形学图形旋转_计算机图形学中的平板显示

计算机图形学图形旋转平板显示器 (Flat Panel Display) It is generally known as FPD, the flat-panel display is such a display technology which overtakes Cathode Ray Tube as a new standard of computer desktop displays. Unlike monitors through CRT, flat-panel d…

一文掌握Redisson分布式锁原理|干货推荐

ReentrantLock 重入锁在说 Redisson 之前我们先来说一下 JDK 可重入锁: ReentrantLockReentrantLock 保证了 JVM 共享资源同一时刻只允许单个线程进行操作实现思路ReentrantLock 内部公平锁与非公平锁继承了 AQS[AbstractQueuedSynchronizer]1、AQS 内部通过 volatil 修饰的 in…

android创建文件夹和文件的一些经验教训

这几天做一个功能需要在手机上创建一个文件夹&#xff0c;然后往里面存储一些文件&#xff0c;首先得考虑用户有没有sdcard&#xff0c;如果有就在sdcard上创建一个指定的文件夹&#xff0c;如果没有则在你的工程所在的目录“/data/data/你的包名”下创建文件夹。用到的方法是&…

7种分布式事务的解决方案,一次讲给你听

本文约5300字&#xff0c;阅读时长「5分钟」什么是分布式事务分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器「分别位于不同的分布式系统的不同节点之上」。一个大的操作由N多的小的操作共同完成。而这些小的操作又分布在不同的服务上。针对于这些操…

js中的转译_JavaScript中的填充和转译

js中的转译JavaScript is rapidly advancing. Today its the most popular programming/scripting language that devs use to code logic and applications and is used in umpteen number of places. The community behind its development hasnt stopped creating and addin…

css @media 响应式布局

2019独角兽企业重金招聘Python工程师标准>>> &#xfeff;1、在 html 标签中 <link rel"stylesheet" type"text/css" href"style1.css" media"screen and (min-width: 600px) and (max-width: 800px)"> 2、在样式表中…

Apache JK Tomcat 集群问题

2019独角兽企业重金招聘Python工程师标准>>> 这几天被集群并发问题快折腾死了&#xff0c;望哪位高人看下到底是哪里出现了问题。 Apache Server是正常的&#xff0c;各服务器的Tomcat 也是正常的&#xff0c;但当Apache的连接数达到 300左右的时候&#xff0c;JK就…

Redis实现分布式锁的7种方案,及正确使用姿势!

种方案前言日常开发中&#xff0c;秒杀下单、抢红包等等业务场景&#xff0c;都需要用到分布式锁。而Redis非常适合作为分布式锁使用。本文将分七个方案展开&#xff0c;跟大家探讨Redis分布式锁的正确使用方式。如果有不正确的地方&#xff0c;欢迎大家指出哈&#xff0c;一起…

c#中的long类型示例_C#中带示例的无符号字节数组

c#中的long类型示例C&#xff03;中的无符号字节数组 (Unsigned Byte Array in C#) In C#.Net, we can create an unsigned byte array by using byte, byte is used to store only positive values between the range of 0 to 255 (Unsigned 8 bits integer). 在C&#xff03;…