前言
最近由于項目需求,項目中需要實現一個WebSSH連接終端的功能,由于自己第一次做這類型功能,所以首先上了GitHub找了找有沒有現成的輪子可以拿來直接用,當時看到了很多這方面的項目,例如:GateOne、webssh、shellinabox等,這些項目都可以很好地實現webssh的功能。
但是最終并沒有采用,原因是在于這些底層大都是python寫的,需要依賴很多文件,自己用的時候可以使用這種方案,快捷省事,但是做到項目中供用戶使用時,總不能要求用戶做到服務器中必須包含這些底層依賴,這顯然不太合理,所以我決定自己動手寫一個WebSSH的功能,并且作為一個獨立的項目開源出來。
github項目開源地址:https://github.com/NoCortY/WebSSH
由于webssh需要實時數據交互,所以會選用長連接的WebSocket,為了開發的方便,框架選用SpringBoot,另外還自己了解了Java用戶連接ssh的jsch和實現前端shell頁面的xterm.js.
javaspring、所以,最終的技術選型就是 SpringBoot+Websocket+jsch+xterm.js。
<parent>
????<groupId>org.springframework.bootgroupId>
????<artifactId>spring-boot-starter-parentartifactId>
????<version>2.1.7.RELEASEversion>
????<relativePath?/>?
parent>
<dependencies>
????
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-webartifactId>
????dependency>
????
????<dependency>
????????<groupId>com.jcraftgroupId>
????????<artifactId>jschartifactId>
????????<version>0.1.54version>
????dependency>
????
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-websocketartifactId>
????dependency>
????
????<dependency>
????????<groupId>commons-iogroupId>
????????<artifactId>commons-ioartifactId>
????????<version>1.4version>
????dependency>
????<dependency>
????????<groupId>commons-fileuploadgroupId>
????????<artifactId>commons-fileuploadartifactId>
????????<version>1.3.1version>
????dependency>
dependencies>
由于xterm是一個冷門技術,所以很多同學并沒有這方面的知識支撐,我也是為了實現這個功能所以臨時學的,所以在這給大家介紹一下。
xterm.js是一個基于WebSocket的容器,它可以幫助我們在前端實現命令行的樣式。就像是我們平常再用SecureCRT或者XShell連接服務器時一樣。
下面是官網上的入門案例:
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。
javaweb的session、由于xterm只要只是實現了前端的樣式,并不能真正地實現與服務器交互,與服務器交互主要還是靠我們Java后端來進行控制的,所以我們從后端開始,使用jsch+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("*");
??????}
??}
剛才我們完成了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?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項目,沒有用戶模塊,如果需要將這個項目集成到自己的項目中,需要修改這部分代碼,將其改為自己項目中識別一個用戶所用的用戶標識。
java自己寫session,剛才我們實現了websocket的配置,都是一些死代碼,實現了接口再根據自身需求即可實現,現在我們將進行后端主要業務邏輯的實現,在實現這個邏輯之前,我們先來想想,WebSSH,我們主要想要呈現一個什么效果。
我這里做了一個總結:
1.首先我們得先連接上終端(初始化連接)
2.其次我們的服務端需要處理來自前端的消息(接收并處理前端消息)
3.我們需要將終端返回的消息回寫到前端(數據回寫前端)
4.關閉連接
javasession詳解、根據這四個需求,我們先定義一個接口,這樣可以讓需求明了起來。
??/**
???*?@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.處理客戶端發送的數據
在這一步驟中,我們會分為兩個分支。
java中的session?第一個分支:如果客戶端發來的是終端的用戶名和密碼等信息,那么我們進行終端的連接。
第二個分支:如果客戶端發來的是操作終端的命令,那么我們就直接轉發到終端并且獲取終端的執行結果。
具體代碼實現:
?????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);
?????????????}
?????}
至此,我們的整個后端實現就結束了,由于篇幅有限,這里將一些操作封裝成了方法,就不做過多展示了,重點講邏輯實現的思路吧。接下來我們將進行前端的實現。
java如何利用session、前端工作主要分為這么幾個步驟:
頁面的實現
連接WebSocket并完成數據的接收并回寫
數據的發送
所以我們一步一步來實現它。
頁面的實現很簡單,我們只不過需要在一整個屏幕上都顯示終端那種大黑屏幕,所以我們并不用寫什么樣式,只需要創建一個div,之后將terminal實例通過xterm放到這個div中,就可以實現了。
??html>
??<html>
??<head>
??????<title>WebSSHtitle>
??????<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>
??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);
??????????????}
??????????});
??????}
java獲取session的值。ls命令:
vim編輯器:
top命令:
這樣我們就完成了一個webssh項目的實現,沒有依賴其它任何的組件,后端完全使用Java實現,由于用了SpringBoot,非常容易部署。
但是,我們還可以對這個項目進行擴展,比如新增上傳或下載文件,就像Xftp一樣,可以很方便地拖拽式上傳下載文件。
這個項目之后我會持續更新,上述功能也會慢慢實現,Github:https://github.com/NoCortY/WebSSH
java獲取當前session?喜歡可以給個Star哦~
歡迎關注,點個在看
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态