純java實現(xiàn)瀏覽器操作Linux服務器
技術(shù)選型
由于webssh需要實時數(shù)據(jù)交互,所以會選用長連接的WebSocket庇楞,為了開發(fā)的方便榜配,框架選用SpringBoot,另外還自己了解了Java用戶連接ssh的jsch和實現(xiàn)前端shell頁面的xterm.js.
所以吕晌,最終的技術(shù)選型就是 SpringBoot+Websocket+jsch+xterm.js芥牌。
實現(xiàn)思路
導入依賴
<dependencies>
<!-- Web相關(guān) -->
<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.3</version>
</dependency>
</dependencies>
<!--打jar包-->
<build>
<finalName>WebSSH</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
引入xterm.js
xterm.js是一個基于WebSocket的容器,它可以幫助我們在前端實現(xiàn)命令行的樣式聂使。就像是我們平常再用SecureCRT或者XShell連接服務器時一樣壁拉。
后端實現(xiàn)
由于xterm只要只是實現(xiàn)了前端的樣式,并不能真正地實現(xiàn)與服務器交互柏靶,與服務器交互主要還是靠我們Java后端來進行控制的弃理,所以我們從后端開始,使用jsch+websocket實現(xiàn)這部分內(nèi)容屎蜓。
WebSocket配置
由于消息實時推送到前端需要用到WebSocket痘昌,不了解WebSocket的同學可以先去自行了解一下,這里就不過多介紹了炬转,我們直接開始進行WebSocket的配置辆苔。
/**
* @Description: websocket配置
*/
@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)的實現(xiàn)
剛才我們完成了WebSocket的配置扼劈,并指定了一個處理器和攔截器驻啤。所以接下來就是處理器和攔截器的實現(xiàn)。
- 攔截器:
public class WebSocketInterceptor implements HandshakeInterceptor {
/**
* @Description: Handler處理前調(diào)用
* @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
* @return: boolean
*/
@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處理器
*/
@Component
public class WebSSHWebSocketHandler implements WebSocketHandler{
@Autowired
private WebSSHService webSSHService;
private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);
/**
* @Description: 用戶連接上WebSocket的回調(diào)
* @Param: [webSocketSession]
* @return: void
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
logger.info("用戶:{},連接WebSSH", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY));
//調(diào)用初始化連接
webSSHService.initConnection(webSocketSession);
}
/**
* @Description: 收到消息的回調(diào)
* @Param: [webSocketSession, webSocketMessage]
* @return: void
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
if (webSocketMessage instanceof TextMessage) {
logger.info("用戶:{},發(fā)送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
//調(diào)用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: 出現(xiàn)錯誤的回調(diào)
* @Param: [webSocketSession, throwable]
* @return: void
* @Author: mcl
* @Date: 2021/11/3
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
logger.error("數(shù)據(jù)傳輸錯誤");
}
/**
* @Description: 連接關(guān)閉的回調(diào)
* @Param: [webSocketSession, closeStatus]
* @return: void
* @Author: mcl
* @Date: 2021/11/3
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
logger.info("用戶:{}斷開webssh連接", String.valueOf(webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY)));
//調(diào)用service關(guān)閉連接
webSSHService.close(webSocketSession);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
需要注意的是贼涩,在攔截器中加入的用戶標識是使用了隨機的UUID,這是因為作為一個獨立的websocket項目薯蝎,沒有用戶模塊遥倦,如果需要將這個項目集成到自己的項目中,需要修改這部分代碼占锯,將其改為自己項目中識別一個用戶所用的用戶標識袒哥。
梳理下處理邏輯
1.首先我們得先連接上終端(初始化連接)
2.其次我們的服務端需要處理來自前端的消息(接收并處理前端消息)
3.我們需要將終端返回的消息回寫到前端(數(shù)據(jù)回寫前端)
4.關(guān)閉連接
按照這個邏輯,定義個接口烟央。
/**
* @Description: WebSSH的業(yè)務邏輯
* @Author: mcl
* @Date: 2021/11/3
*/
public interface WebSSHService {
/**
* @Description: 初始化ssh連接
* @Param:
* @return:
* @Date: 2021/11/3
*/
public void initConnection(WebSocketSession session);
/**
* @Description: 處理客戶段發(fā)的數(shù)據(jù)
* @Param:
* @return:
* @Date: 2021/11/3
*/
public void recvHandle(String buffer, WebSocketSession session);
/**
* @Description: 數(shù)據(jù)寫回前端 for websocket
* @Param:
* @return:
* @Date: 2021/11/3
*/
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;
/**
* @Description: 關(guān)閉連接
* @Param:
* @return:
* @Date: 2021/11/3
*/
public void close(WebSocketSession session);
}
實現(xiàn)功能
- 初始化連接
由于我們的底層是依賴jsch實現(xiàn)的统诺,所以這里是需要使用jsch去建立連接的。而所謂初始化連接疑俭,實際上就是將我們所需要的連接信息粮呢,保存在一個Map中,這里并不進行任何的真實連接操作。為什么這里不直接進行連接啄寡?因為這里前端只是連接上了WebSocket豪硅,但是我們還需要前端給我們發(fā)來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);
}
- 處理客戶端發(fā)送的數(shù)據(jù)
在這一步驟中,我們會分為兩個分支识藤。
第一個分支:如果客戶端發(fā)來的是終端的用戶名和密碼等信息砚著,那么我們進行終端的連接。
第二個分支:如果客戶端發(fā)來的是操作終端的命令痴昧,那么我們就直接轉(zhuǎn)發(fā)到終端并且獲取終端的執(zhí)行結(jié)果稽穆。
具體代碼實現(xiàn):
public void recvHandle(String buffer, WebSocketSession session) {
ObjectMapper objectMapper = new ObjectMapper();
WebSSHData webSSHData = null;
try {
//轉(zhuǎn)換前端發(fā)送的JSON
webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
} catch (IOException e) {
logger.error("Json轉(zhuǎn)換異常");
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())) {
//如果是發(fā)送命令的請求
String command = webSSHData.getCommand();
SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
if (sshConnectInfo != null) {
try {
//發(fā)送命令到終端
transToSSH(sshConnectInfo.getChannel(), command);
} catch (IOException e) {
logger.error("webssh連接異常");
logger.error("異常信息:{}", e.getMessage());
close(session);
}
}
} else {
logger.error("不支持的操作");
close(session);
}
}
- 數(shù)據(jù)通過websocket發(fā)送到前端
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
session.sendMessage(new TextMessage(buffer));
}
- 關(guān)閉連接
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);
}
}
前端實現(xiàn)
前端工作主要分為這么幾個步驟:
- 頁面的實現(xiàn)
- 連接WebSocket并完成數(shù)據(jù)的接收并回寫
- 數(shù)據(jù)的發(fā)送
頁面實現(xiàn)
頁面的實現(xiàn)很簡單,我們只不過需要在一整個屏幕上都顯示終端那種大黑屏幕赶撰,所以我們并不用寫什么樣式舌镶,只需要創(chuàng)建一個div,之后將terminal實例通過xterm放到這個div中豪娜,就可以實現(xiàn)了餐胀。
<!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="../js/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>
</body>
</html>
連接WebSocket并完成數(shù)據(jù)的發(fā)送、接收瘤载、回寫
<script>
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) {
//鍵盤輸入時的回調(diào)函數(shù)
client.sendClientData(data);
});
term.open(document.getElementById('terminal'));
//在頁面上顯示連接中...
term.write('Connecting...');
//執(zhí)行連接操作
client.connect({
onError: function (error) {
//連接失敗回調(diào)
term.write('Error: ' + error + '\r\n');
},
onConnect: function () {
//連接成功回調(diào)
client.sendInitData(options);
},
onClose: function () {
//連接關(guān)閉回調(diào)
term.write("\rconnection closed");
},
onData: function (data) {
//收到數(shù)據(jù)時回調(diào)
term.write(data);
}
});
}
</script>
效果展示
要不自己腦補下否灾?