предисловие
В последнее время из-за требований проекта в проекте необходимо реализовать функцию терминала подключения WebSSH.Поскольку я впервые делаю такую функцию, я сначала отправился на GitHub, чтобы узнать, есть ли готовые колеса, которые могут можно использовать напрямую.Я много такого видел.Аспекты проектов,таких как:GateOne,webssh,shellinabox и т.д.,эти проекты вполне могут реализовывать функции webssh,но они не были приняты в итоге,т.к. большинство из этих нижние слои написаны на python и должны полагаться на множество файлов.Это решение можно использовать время от времени, что быстро и просто, но когда проект используется пользователями, невозможно требовать от пользователей включения этих базовых зависимостей в server, что явно неразумно, поэтому я решил написать функцию WebSSH самостоятельно и открыть исходный код как независимый проект.
Адрес с открытым исходным кодом проекта github:GitHub.com/no Cort Y/веб…
Технический отбор
Так как webssh требует взаимодействия с данными в режиме реального времени, он будет использовать давно подключенный WebSocket.Для удобства разработки фреймворк использует SpringBoot.Кроме того, я также узнал о jsch для пользователей Java для подключения к ssh и 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>
В финальном тесте страница выглядит так:
Вы можете увидеть, что страница имеет стиль, похожий на стиль оболочки, затем углубитесь и реализуйте webssh в соответствии с этим.
бэкэнд реализация
Поскольку 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<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处理器 * @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, это потому, что в качестве независимого проекта веб-сокета нет пользовательского модуля.Если вам нужно интегрировать этот проект в свой собственный проект, вам нужно изменить эту часть кода и изменить его на идентификатор пользователя, используемый для идентификации пользователя в ваш собственный проект.
-
Реализация бизнес-логики 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); }
Теперь мы можем реализовать функции, которые мы определили в соответствии с этим интерфейсом.
-
инициализировать соединение
Поскольку наш нижний уровень реализован с использованием 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); }
-
Обработка данных, отправленных клиентом
На этом этапе мы разделимся на две ветви.
Первая ветвь: Если клиент отправляет информацию типа имени пользователя и пароля терминала, то подключаем терминал.
Вторая ветвь: если клиент отправляет команду на работу терминала, то мы напрямую пересылаем ее на терминал и получаем результат выполнения терминала.
Конкретная реализация кода:
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); } }
-
Данные отправляются на внешний интерфейс через веб-сокет
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException { session.sendMessage(new TextMessage(buffer)); }
-
закрыть соединение
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); } }
На этом вся наша back-end реализация закончена.Из-за ограниченного места некоторые операции здесь инкапсулированы в методы, поэтому я не буду их слишком много показывать.Давайте сосредоточимся на идее реализации логики. Далее мы реализуем интерфейсную реализацию.
-
Интерфейсная реализация
Предварительная работа в основном делится на следующие этапы:
- Реализация страницы
- Подключите WebSocket и завершите получение данных и обратную запись
- отправка данных
Итак, давайте реализуем это шаг за шагом.
-
реализация страницы
Реализация страницы очень проста, нам просто нужно отобразить большой черный экран терминала на весь экран, поэтому нам не нужно писать какой-либо стиль, просто создайте div, а затем поместите экземпляр терминала в этот div через xterm, Этого можно добиться.
<!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); } }); }
Показать результаты
-
соединять
-
соединение успешно
-
Командная операция
команда лс:
вим-редактор:
верхняя команда:
Эпилог
Таким образом, мы завершили реализацию проекта webssh, не полагаясь на какие-либо другие компоненты. Бэкенд полностью реализован на Java. Благодаря использованию SpringBoot его очень легко развернуть.
Однако мы также можем расширить этот проект, например, добавив файлы для загрузки или скачивания, как и Xftp, вы можете легко перетаскивать файлы для загрузки и скачивания.
После этого проекта я продолжу обновление, а вышеперечисленные функции будут реализовываться потихоньку.Github:GitHub.com/no Cort Y/веб…
Если вам это нравится, вы можете поставить ему звезду~
Добро пожаловать в мой личный блог:Object's Blog