使用java语言实现web ssh客户端,使用websocket的stomp协议+xterm.js实现
效果如下:
0、引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</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>
</dependencies>
1、定义两个实体
package com.iscas.ssh.server.model;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import lombok.Data;
import org.springframework.web.socket.WebSocketSession;
/**
* SSH连接信息
*
* @author zhuquanwen
* @vesion 1.0
* @date 2020/12/27 14:07
* @since jdk1.8
*/
@Data
public class SSHConnection {
private WebSocketSession webSocketSession;
private String connectionId;
private JSch jSch;
private Channel channel;
}
package com.iscas.ssh.server.model;
import lombok.Data;
/**
* SSH信息
*
* @author zhuquanwen
* @vesion 1.0
* @date 2020/12/27 14:09
* @since jdk1.8
*/
@Data
public class WebSSHData {
//操作
private String operate;
private String host;
//端口号默认为22
private Integer port = 22;
private String username;
private String password;
private String command = "";
private String connectionId;
}
2、定义service,通过JSCH建立SSH连接,并发送前端用户的输入,接收返回结果。
package com.iscas.ssh.server.service;
import com.iscas.ssh.server.constant.CommonConstants;
import com.iscas.ssh.server.model.SSHConnection;
import com.iscas.ssh.server.model.WebSSHData;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.security.Principal;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 处理SSH连接的业务
*
* @author zhuquanwen
* @vesion 1.0
* @date 2020/12/27 14:10
* @since jdk1.8
*/
@Service
@Slf4j
public class SSHService {
//存放ssh连接信息的map
private static Map<String, Object> sshMap = new ConcurrentHashMap<>();
//连接ID对应的用户
private static Map<String, String> connectionUserMap = new ConcurrentHashMap<>();
//线程池
private ExecutorService executorService = Executors.newCachedThreadPool();
private int connectionTimeout = 30;
private int channelTimeout = 3;
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 初始化连接
*/
public void initConnection(String connectionId, Principal user) {
JSch jSch = new JSch();
SSHConnection sshConnection = new SSHConnection();
sshConnection.setJSch(jSch);
sshConnection.setConnectionId(connectionId);
// 将这个ssh连接信息放入map中
sshMap.put(connectionId, sshConnection);
connectionUserMap.put(connectionId, user.getName());
}
/**
* @Description: 处理客户端发送的数据
*/
public void recvHandle(WebSSHData webSSHData) throws IOException, JSchException {
String connectionId = webSSHData.getConnectionId();
if (CommonConstants.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
//找到刚才存储的ssh连接对象
SSHConnection sshConnection = (SSHConnection) sshMap.get(connectionId);
//启动线程异步处理
WebSSHData finalWebSSHData = webSSHData;
executorService.execute(new Runnable() {
@Override
public void run() {
try {
connectToSSH(sshConnection, finalWebSSHData);
} catch (JSchException | IOException e) {
log.error("webssh连接异常", e.getMessage());
close(connectionId);
}
}
});
} else if (CommonConstants.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
String command = webSSHData.getCommand();
SSHConnection sshConnection = (SSHConnection) sshMap.get(connectionId);
if (sshConnection != null) {
try {
transToSSH(sshConnection.getChannel(), command);
} catch (IOException e) {
log.error("webssh连接异常", e.getMessage());
close(connectionId);
}
}
} else {
log.error("不支持的操作");
close(connectionId);
}
}
public void sendMessage(String connectionId, byte[] buffer) throws IOException {
String username = connectionUserMap.get(connectionId);
if (username == null) {
throw new RuntimeException(String.format("未找到connectionId:[%s]对应的websocket连接用户", connectionId));
}
messagingTemplate.convertAndSendToUser(username, "/queue/".concat(connectionId), new String(buffer, "utf-8"));
}
public void sendMessage(String connectionId, String data) throws IOException {
String username = connectionUserMap.get(connectionId);
if (username == null) {
throw new RuntimeException(String.format("未找到connectionId:[%s]对应的websocket连接用户", connectionId));
}
messagingTemplate.convertAndSendToUser(username, "/queue/".concat(connectionId), data);
}
public void close(String connectionId) {
SSHConnection sshConnection = (SSHConnection) sshMap.get(connectionId);
if (sshConnection != null) {
//断开连接
if (sshConnection.getChannel() != null) sshConnection.getChannel().disconnect();
//map中移除
sshMap.remove(connectionId);
}
}
/**
* 使用jsch连接终端
*/
private void connectToSSH(SSHConnection sshConnection, WebSSHData webSSHData) throws JSchException, IOException {
Session session = null;
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
//获取jsch的会话
session = sshConnection.getJSch().getSession(webSSHData.getUsername(), webSSHData.getHost(), webSSHData.getPort());
session.setConfig(config);
//设置密码
session.setPassword(webSSHData.getPassword());
//连接 超时时间30s
session.connect(connectionTimeout * 1000);
//开启shell通道
Channel channel = session.openChannel("shell");
//通道连接 超时时间3s
channel.connect(channelTimeout * 1000);
//设置channel
sshConnection.setChannel(channel);
//转发消息
transToSSH(channel, "\r\n");
// //读取终端返回的信息流
InputStream inputStream = channel.getInputStream();
// try {
// InputStreamReader isr = new InputStreamReader(inputStream);
// BufferedReader br = new BufferedReader(isr);
// String line = null;
// while ((line = br.readLine()) != null) {
// sendMessage(sshConnection.getConnectionId(), line);
// }
// } finally {
// //断开连接后关闭会话
// session.disconnect();
// channel.disconnect();
// if (inputStream != null) {
// inputStream.close();
// }
// }
try {
//循环读取
byte[] buffer = new byte[1024];
int i = 0;
//如果没有数据来,线程会一直阻塞在这个地方等待数据。
// byte[] toSendBytes = null;
// List<Byte> bytes = new ArrayList<>();
// List<Byte> lastBytes = new ArrayList<>();
// while ((i = inputStream.read(buffer)) != -1) {
// for (int j = 0; j < i; j++) {
//
// byte b = buffer[j];
// System.out.print((char)b);
// lastBytes.add(b);
// if (b == '\n' || b == '\r' || b == '>') {
// bytes.addAll(lastBytes);
// lastBytes.clear();
// }
// }
// if (bytes.size() > 0) {
// toSendBytes = new byte[bytes.size()];
// for (int i1 = 0; i1 < bytes.size(); i1++) {
// toSendBytes[i1] = bytes.get(i1);
// }
// sendMessage(webSSHData.getConnectionId(), toSendBytes);
// }
// }
while ((i = inputStream.read(buffer)) != -1) {
sendMessage(webSSHData.getConnectionId(), Arrays.copyOfRange(buffer, 0, i));
}
} finally {
//断开连接后关闭会话
session.disconnect();
channel.disconnect();
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* 将消息转发到终端
*/
private void transToSSH(Channel channel, String command) throws IOException {
if (channel != null) {
OutputStream outputStream = channel.getOutputStream();
outputStream.write(command.getBytes());
outputStream.flush();
}
}
private void transToSSH(PrintWriter pw, String command) throws IOException {
if (pw != null) {
pw.println(command);
pw.flush();
}
}
}
3、定义一个controller,接收前端的websocket消息,websocket+stomp的配置的过程略过了,可以翻阅之前的文章
package com.iscas.ssh.server.controller;
import com.iscas.ssh.server.model.WebSSHData;
import com.iscas.ssh.server.service.SSHService;
import com.iscas.templet.common.BaseController;
import com.iscas.templet.common.ResponseEntity;
import com.jcraft.jsch.JSchException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.security.Principal;
/**
* ssh连接控制器
*
* @author zhuquanwen
* @vesion 1.0
* @date 2020/12/27 14:26
* @since jdk1.8
*/
@RestController
@Api(tags = "SSH连接控制器")
public class WebSSHCotroller extends BaseController {
@Autowired
private SSHService sshService;
@MessageMapping("/connect")
@ApiOperation(value="开启一个新的会话连接窗口-2020-12-27", notes="create by:朱全文")
@ApiImplicitParams(
{
@ApiImplicitParam(name = "sshData", value = "连接信息", required = true, dataType = "WebSSHData")
}
)
public ResponseEntity connect(Principal user, WebSSHData sshData) throws IOException, JSchException {
ResponseEntity response = getResponse();
String connectionId = sshData.getConnectionId();
sshService.initConnection(connectionId, user);
sshService.recvHandle(sshData);
return response;
}
@MessageMapping("/command")
@ApiOperation(value="开启一个新的会话连接窗口-2020-12-27", notes="create by:朱全文")
@ApiImplicitParams(
{
@ApiImplicitParam(name = "sshData", value = "命令信息", required = true, dataType = "WebSSHData")
}
)
public ResponseEntity command(Principal user, WebSSHData sshData) throws IOException, JSchException {
ResponseEntity response = getResponse();
sshService.recvHandle(sshData);
return response;
}
}
4、用到的常量
package com.iscas.ssh.server.constant;
/**
*
* 公用常量
* @author zhuquanwen
* @vesion 1.0
* @date 2020/12/27 14:05
* @since jdk1.8
*/
public interface CommonConstants {
/**
* 随机生成uuid的key名
*/
String USER_UUID_KEY = "user_uuid";
/**
* 发送指令:连接
*/
String WEBSSH_OPERATE_CONNECT = "connect";
/**
* 发送指令:命令
*/
String WEBSSH_OPERATE_COMMAND = "command";
}
5、JS+HTML代码,xterm.js+xterm.css+jquery+sockjs+stompjs可以从网上下载
function WSSHClient() {
};
var stompClient = null;
WSSHClient.prototype.connect = function (options) {
// var endpoint = this._generateEndpoint();
if (window.WebSocket) {
//如果支持websocket
var socket = new SockJS("http://localhost:7901/demo/webSsh");
stompClient = Stomp.over(socket);
var connectionId = options.connectInfo.connectionId;
stompClient.connect({
Authorization: "这是一个随机数"
},
function connectCallback(frame) {
// 连接成功时(服务器响应 CONNECTED 帧)的回调方法
// alert("success");
subscribe(connectionId, options);
options.onConnect();
},
function errorCallBack(error) {
// 连接失败时(服务器响应 ERROR 帧)的回调方法
options.onError('连接失败');
});
}else {
//否则报错
options.onError('WebSocket Not Supported');
return;
}
// this._connection.onopen = function () {
// options.onConnect();
// };
//
// this._connection.onmessage = function (evt) {
// var data = evt.data.toString();
// //data = base64.decode(data);
// options.onData(data);
// };
//
//
// this._connection.onclose = function (evt) {
// options.onClose();
// };
};
//订阅消息
function subscribe(connectionId, options) {
stompClient.subscribe('/user/queue/' + connectionId, function (response) {
console.log("你接收到的消息为:" + response);
options.onData(response.body);
});
}
WSSHClient.prototype.send = function (data) {
this._connection.send(JSON.stringify(data));
};
WSSHClient.prototype.sendInitData = function (options) {
//连接参数
stompClient.send("/app/connect", {}, JSON.stringify(options));
}
WSSHClient.prototype.sendClientData = function (connectionId, data) {
//发送指令
stompClient.send("/app/command", {}, JSON.stringify({"operate": "command", "command": data, "connectionId": connectionId}))
}
var client = new WSSHClient();
<html>
<head>
<meta charset="UTF-8">
<title>WebSSH</title>
<link rel="stylesheet" href="../css/xterm.css" />
<script src="../js/jquery-3.4.1.min.js"></script>
<script src="../js/xterm.js" charset="utf-8"></script>
<script src="../js/stomp.min.js"></script>
<script src="../js/sockjs.min.js"></script>
<script src="../js/webssh.js" charset="utf-8"></script>
<script>
function connect() {
var ip = document.getElementById('ip').value;
var port = document.getElementById('port').value;
var username = document.getElementById('username').value;
var pwd = document.getElementById('pwd').value;
openTerminal( {
operate:'connect',
host: ip,//IP
port: port,//端口号
username: username,//用户名
password: pwd, //密码
connectionId: 'xxxx'
});
document.getElementById("terminal").style.display = "block";
document.getElementById("input").style.display = "none";
}
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(options.connectionId, data);
});
term.open(document.getElementById('terminal'));
//在页面上显示连接中...
term.write('Connecting...');
//执行连接操作
client.connect({
connectInfo: options,
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);
}
});
}
</script>
</head>
<body>
<div id="terminal" style="width: 100%;height: 100%; display: none;" ></div>
<div id = "input" style="display:block;">
IP:<input id = "ip" value="localhost"/><br/>
端口:<input id="port" value="22"/><br/>
用户名:<input id="username" value="root"/><br/>
密码:<input id="pwd" value=""/><br/>
<input type="submit" value="连接" onclick="connect();"/>
</div>
</body>
</html>