純java實現(xiàn)瀏覽器操作Linux服務器

純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連接服務器時一樣壁拉。

CMUuq85pI.png

后端實現(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>

效果展示

要不自己腦補下否灾?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惕虑,隨后出現(xiàn)的幾起案子坟冲,更是在濱河造成了極大的恐慌,老刑警劉巖溃蔫,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異琳猫,居然都是意外死亡伟叛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門脐嫂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來统刮,“玉大人,你說我怎么就攤上這事账千〗拿桑” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵匀奏,是天一觀的道長鞭衩。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么论衍? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任瑞佩,我火速辦了婚禮,結(jié)果婚禮上坯台,老公的妹妹穿的比我還像新娘炬丸。我一直安慰自己,他們只是感情好蜒蕾,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布稠炬。 她就那樣靜靜地躺著,像睡著了一般咪啡。 火紅的嫁衣襯著肌膚如雪首启。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天瑟匆,我揣著相機與錄音闽坡,去河邊找鬼。 笑死愁溜,一個胖子當著我的面吹牛疾嗅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冕象,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼代承,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了渐扮?” 一聲冷哼從身側(cè)響起论悴,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎墓律,沒想到半個月后膀估,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡耻讽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年察纯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片针肥。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡饼记,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出慰枕,到底是詐尸還是另有隱情具则,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布具帮,位于F島的核電站博肋,受9級特大地震影響低斋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜束昵,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一拔稳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锹雏,春花似錦巴比、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佣耐,卻和暖如春政勃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背兼砖。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工奸远, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人讽挟。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓懒叛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耽梅。 傳聞我的和親對象是個殘疾皇子薛窥,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355