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現在我們可以根據這個接口去實現我們定義的功能了。
初始化連接
由于我們的底層是依賴jsch實現的,所以這里是需要使用jsch去建立連接的。而所謂初始化連接,實際上就是將我們所需要的連接信息,保存在一個Map中,這里并不進行任何的真實連接操作。為什么這里不直接進行連接?因為這里前端只是連接上了WebSocket,但是我們還需要前端給我們發來linux終端的用戶名和密碼,沒有這些信息,我們是無法進行連接的。
處理客戶端發送的數據
在這一步驟中,我們會分為兩個分支。
第一個分支:如果客戶端發來的是終端的用戶名和密碼等信息,那么我們進行終端的連接。
第二個分支:如果客戶端發來的是操作終端的命令,那么我們就直接轉發到終端并且獲取終端的執行結果。
具體代碼實現:
數據通過websocket發送到前端
關閉連接
至此,我們的整個后端實現就結束了,由于篇幅有限,這里將一些操作封裝成了方法,就不做過多展示了,重點講邏輯實現的思路吧。接下來我們將進行前端的實現。
前端實現
前端工作主要分為這么幾個步驟:
頁面的實現
連接WebSocket并完成數據的接收并回寫
數據的發送
所以我們一步一步來實現它。
頁面實現
頁面的實現很簡單,我們只不過需要在一整個屏幕上都顯示終端那種大黑屏幕,所以我們并不用寫什么樣式,只需要創建一個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 單例了
總結
以上是生活随笔為你收集整理的Java打造一款SSH客户端,已开源!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构树二叉树计算节点_查找二叉树中叶
- 下一篇: 为什么阿里全面推动 K8S 落地,咬紧牙