java实现web ssh客户端

使用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>
  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值