SSH客户端(Java实现)
写在前⾔
果⼦在(程序员编程)中看到⼀个很好的项⽬。虽然平时⽤不到,但是对于⾃⼰理解SpringBoot,⽹络通信还是有好处的。所以就摘录如下,本⽂并不是全⽂照搬,会做出修改润饰,并加⼊⾃⼰的理解。⽂末会注明来源,如有侵权,敬请告知。
1、需求⽬标:
⼿写⼀个可以实现WebSSH连接终端功能的项⽬
2、技术选型
SpringBoot+Websocket+jsch+xterm.js ,理由如下:
由于webssh需要实时数据交互,所以会选⽤长连接的WebSocket;
为了开发的⽅便,框架选⽤SpringBoot;
另外还⾃⼰了解了Java⽤户连接ssh的jsch和实现前端shell页⾯的xterm.js.
3、依赖导⼊
<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>
4、⼀个简单的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.ElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>
最终测试,页⾯就是下⾯这个样⼦:
可以看到页⾯已经出现了类似与shell的样式,那就根据这个继续深⼊,实现⼀个webssh。
5、后端实现
由于xterm只要只是实现了前端的样式,并不能真正地实现与服务器交互,与服务器交互主要还是靠我们Java后端来进⾏控制的,所以我们从后端开始,使⽤
jsch+websocket实现这部分内容。
5.1、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("*");
}
}
5.2、处理器(Handler)和(Interceptor)的实现
实现完WebSocket的配置,并指定了⼀个处理器和。
接下来对处理器和进⾏实现:
1public class WebSocketInterceptor implements HandshakeInterceptor {
2/**
3    * @Description: Handler处理前调⽤
4    * @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
5    * @return: boolean
6    * @Author: NoCortY
7    * @Date: 2020/3/1
8*/
9    @Override
10public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception { 11if (serverHttpRequest instanceof ServletServerHttpRequest) {
12            ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
13//⽣成⼀个UUID,这⾥由于是独⽴的项⽬,没有⽤户模块,所以可以⽤随机的UUID
14//但是如果要集成到⾃⼰的项⽬中,需要将其改为⾃⼰识别⽤户的标识
15            String uuid = UUID.randomUUID().toString().replace("-","");
16//将uuid放到websocketsession中
17            map.put(ConstantPool.USER_UUID_KEY, uuid);
18return true;
19        } else {
20return false;
21        }
22    }
23
24    @Override
25public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
26
27    }
28 }
处理器:
/**
* @Description: WebSSH的WebSocket处理器
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Component
public class WebSSHWebSocketHandler implements WebSocketHandler{
@Autowired
private WebSSHService webSSHService;
private Logger logger = Logger(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", Attributes().get(ConstantPool.USER_UUID_KEY));
//调⽤初始化连接
webSSHService.initConnection(webSocketSession);
}
/**
* @Description: 收到消息的回调
* @Param: [webSocketSession, webSocketMessage]
* @return: void
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Override
spring framework是什么框架的public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
if (webSocketMessage instanceof TextMessage) {
logger.info("⽤户:{},发送命令:{}", Attributes().get(ConstantPool.USER_UUID_KEY), String());
//调⽤service接收消息
} 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 {
<("数据传输错误");
}
/**
* @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.Attributes().get(ConstantPool.USER_UUID_KEY)));
//调⽤service关闭连接
webSSHService.close(webSocketSession);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
需要注意的是,这⾥在中加⼊的⽤户标识是使⽤了随机的UUID ,这是因为作为⼀个独⽴的websocket项⽬,没有⽤户模块,如果需要将这个项⽬集成到⾃⼰的项⽬中,需要修改这部分代码,将其改为⾃⼰项⽬中识别⼀个⽤户所⽤的⽤户标识。
5.3、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);
}
现在我们可以根据这个接⼝去实现我们定义的功能了。
⾄此,我们的整个后端实现就结束了。
这⾥将⼀些操作封装成了⽅法,重点在于逻辑实现的思路。
接下来我们将进⾏前端的实现。
5.3.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.Attributes().get(ConstantPool.USER_UUID_KEY));
//将这个ssh连接信息放⼊map中
sshMap.put(uuid, sshConnectInfo);
}
5.3.2、处理客户端发送的数据
在这⼀步骤中,我们会分为两个分⽀。
第⼀个分⽀:如果客户端发来的是终端的⽤户名和密码等信息,那么我们进⾏终端的连接。
第⼆个分⽀:如果客户端发来的是操作终端的命令,那么我们就直接转发到终端并且获取终端的执⾏结果。
具体代码实现:
1public void recvHandle(String buffer, WebSocketSession session) {
2        ObjectMapper objectMapper = new ObjectMapper();
3        WebSSHData webSSHData = null;
4try {
5//转换前端发送的JSON
6            webSSHData = adValue(buffer, WebSSHData.class);
7        } catch (IOException e) {
8            ("Json转换异常");
9            ("异常信息:{}", e.getMessage());
10return;
11        }
12//获取刚才设置的随机的uuid
13        String userId = String.Attributes().get(ConstantPool.USER_UUID_KEY));
14if (ConstantPool.WEBSSH_OPERATE_CONNECT.Operate())) {
15//如果是连接请求
16//到刚才存储的ssh连接对象
17            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) (userId);
18//启动线程异步处理
19            WebSSHData finalWebSSHData = webSSHData;
20            ute(new Runnable() {
21                @Override
22public void run() {
23try {
24//连接到终端
25                        connectToSSH(sshConnectInfo, finalWebSSHData, session);
26                    } catch (JSchException | IOException e) {
27                        ("webssh连接异常");
28                        ("异常信息:{}", e.getMessage());
29                        close(session);
30                    }
31                }
32            });
33        } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.Operate())) {
34//如果是发送命令的请求
35            String command = Command();
36            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) (userId);
37if (sshConnectInfo != null) {
38try {
39//发送命令到终端
40                    Channel(), command);
41                } catch (IOException e) {
42                    ("webssh连接异常");
43                    ("异常信息:{}", e.getMessage());
44                    close(session);
45                }
46            }
47        } else {
48            ("不⽀持的操作");
49            close(session);
50        }
51 }
5.3.3、数据通过websocket发送到前端
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
session.sendMessage(new TextMessage(buffer));
}
5.3.4、关闭连接
public void close(WebSocketSession session) {
//获取随机⽣成的uuid
String userId = String.Attributes().get(ConstantPool.USER_UUID_KEY));
SSHConnectInfo sshConnectInfo = (SSHConnectInfo) (userId);
if (sshConnectInfo != null) {
//断开连接
if (Channel() != null) Channel().disconnect();
//map中移除该ssh连接信息
}
}
6、前端实现
前端⼯作主要分以下三步:
1. 页⾯的实现;
2. 连接WebSocket并完成数据的接收并回写;
3. 数据的发送;
6.1、页⾯实现
前端页⾯只需要在⼀整个屏幕上都显⽰终端那种⼤⿊屏幕,所以我们并不⽤写什么样式,只需要创建⼀个div,之后将terminal实例通过xterm放到这个div中,就可以实现了。
<!doctype html>
<html>
<head>
<title>WebSSH</title>
<link rel="stylesheet" href="../css/xterm.css"/>
</head>
<body>
<div id="terminal" ></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>
6.2、连接WebSocket并完成数据的发送、接收、回写
1 openTerminal( {
2//这⾥的内容可以写死,但是要整合到项⽬中时,需要通过参数的⽅式传⼊,可以动态连接某个终端。
3        operate:'connect',
4        host: 'ip地址',
5        port: '端⼝号',
6        username: '⽤户名',
7        password: '密码'
8    });
9    function openTerminal(options){
10        var client = new WSSHClient();
11        var term = new Terminal({
12            cols: 97,
13            rows: 37,
14            cursorBlink: true, // 光标闪烁
15            cursorStyle: "block", // 光标样式  null | 'block' | 'underline' | 'bar'
16            scrollback: 800, //回滚
17            tabStopWidth: 8, //制表宽度
18            screenKeys: true
19        });
20
21        ('data', function (data) {
22//键盘输⼊时的回调函数
23            client.sendClientData(data);
24        });
25        term.ElementById('terminal'));
26//在页⾯上显⽰连接中...
27        term.write('');
28//执⾏连接操作
29        t({
30            onError: function (error) {
31//连接失败回调
32                term.write('Error: ' + error + '\r\n');
33            },
34            onConnect: function () {
35//连接成功回调
36                client.sendInitData(options);
37            },
38            onClose: function () {
39//连接关闭回调
40                term.write("\rconnection closed");
41            },
42            onData: function (data) {
43//收到数据时回调
44                term.write(data);
45            }
46        });
47    }
7、效果展⽰
7.1、连接

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。