Build a Web‑Based SSH Client with Spring Boot and xterm.js – Step‑by‑Step Guide

This tutorial walks you through creating a browser‑accessible SSH client using Spring Boot, JSch, WebSocket, and xterm.js, covering application scenarios, architecture, core implementation details, file transfer features, performance tweaks, and security best practices for production use.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
Build a Web‑Based SSH Client with Spring Boot and xterm.js – Step‑by‑Step Guide

Introduction

In daily operations, administrators often need to connect to multiple servers via SSH. Traditional SSH clients have limitations that can be addressed by a web‑based solution.

Application Scenarios

Web SSH is useful for internal operations, temporary access, and mobile work environments.

Traditional SSH Limitations

Client dependency – requires installation on each device.

Difficult unified management of configurations and permissions.

Lack of centralized audit logs.

Poor mobile device support.

Firewall restrictions may block SSH ports.

Web SSH Advantages

No client installation – works directly in the browser.

Centralized permission management.

Full operation logging and audit.

Mobile‑friendly interface.

Bypasses port restrictions using HTTP/HTTPS.

Technical Stack

Backend : Spring Boot 3.x, JSch, WebSocket, Spring JdbcTemplate.

Frontend : HTML, JavaScript, xterm.js, WebSocket API.

System Architecture

Browser Terminal ←→ WebSocket ←→ Spring Boot Application ←→ SSH Connection ←→ Target Server
    ↓                                 ↓
    User Interface                     Data Store
    Command Input                     Operation Log
    Result Display                     Configuration Management

Core Process

The user enters SSH connection details in the browser; the Spring Boot backend creates an SSH session via JSch, and terminal data is streamed through WebSocket to the xterm.js component for display.

Implementation Steps

1. Project Initialization

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
  </parent>
  <groupId>com.example</groupId>
  <artifactId>web-ssh-client</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <!-- Spring Boot core -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- WebSocket support -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- JSch for SSH -->
    <dependency>
      <groupId>com.jcraft</groupId>
      <artifactId>jsch</artifactId>
      <version>0.1.55</version>
    </dependency>
    <!-- JDBC and H2 for demo storage -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

2. SSH Connection Manager

public class SSHConnectionManager {
    private final Map<String, Session> connections = new ConcurrentHashMap<>();
    private final Map<String, ChannelShell> channels = new ConcurrentHashMap<>();
    public String createConnection(String host, int port, String username, String password) {
        try {
            JSch jsch = new JSch();
            Session session = jsch.getSession(username, host, port);
            session.setPassword(password);
            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            config.put("PreferredAuthentications", "password");
            session.setConfig(config);
            session.connect(30000);
            ChannelShell channel = (ChannelShell) session.openChannel("shell");
            channel.setPty(true);
            channel.setPtyType("xterm", 80, 24, 640, 480);
            String connectionId = UUID.randomUUID().toString();
            connections.put(connectionId, session);
            channels.put(connectionId, channel);
            log.info("SSH connection established: {}@{}:{}", username, host, port);
            return connectionId;
        } catch (JSchException e) {
            log.error("SSH connection failed", e);
            throw new RuntimeException("SSH connection failed: " + e.getMessage());
        }
    }
    // Additional methods: getChannel, getSession, closeConnection, isConnected
}

3. WebSocket Configuration

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private SSHWebSocketHandler sshWebSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(sshWebSocketHandler, "/ssh")
                .setAllowedOriginPatterns("*");
    }
}

4. WebSocket Handler

@Component
@Slf4j
public class SSHWebSocketHandler extends TextWebSocketHandler {
    @Autowired
    private SSHConnectionManager connectionManager;
    private final Map<WebSocketSession, String> sessionConnections = new ConcurrentHashMap<>();
    private final Map<WebSocketSession, String> sessionUsers = new ConcurrentHashMap<>();
    private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        log.info("WebSocket connection opened: {}", session.getId());
        sessionLocks.put(session, new Object());
    }
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        try {
            JsonNode json = new ObjectMapper().readTree(message.getPayload());
            String type = json.get("type").asText();
            switch (type) {
                case "connect": handleConnect(session, json); break;
                case "command": handleCommand(session, json); break;
                case "resize": handleResize(session, json); break;
                case "disconnect": handleDisconnect(session); break;
                default: log.warn("Unknown message type: {}", type);
            }
        } catch (Exception e) {
            log.error("Failed to process WebSocket message", e);
            sendError(session, "Message processing error: " + e.getMessage());
        }
    }
    // handleConnect, handleCommand, handleResize, handleDisconnect, sendMessage, sendError implementations omitted for brevity
}

5. Server Information Management

@Component
public class ServerConfig {
    private Long id;
    private String name;
    private String host;
    private Integer port;
    private String username;
    private String password;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    // getters and setters omitted
}

@Repository
public class ServerRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    private static final String INSERT_SERVER = """
        INSERT INTO servers (name, host, port, username, password, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    """;
    private static final String SELECT_ALL_SERVERS = """
        SELECT id, name, host, port, username, password, created_at, updated_at FROM servers ORDER BY created_at DESC
    """;
    // CRUD methods using jdbcTemplate omitted for brevity
}

@Service
public class ServerService {
    @Autowired
    private ServerRepository serverRepository;
    public Long saveServer(ServerConfig server) {
        // password encryption can be added here
        return serverRepository.saveServer(server);
    }
    public List<ServerConfig> getAllServers() {
        List<ServerConfig> list = serverRepository.findAllServers();
        list.forEach(s -> s.setPassword(null)); // hide passwords
        return list;
    }
    public Optional<ServerConfig> getServerById(Long id) { return serverRepository.findServerById(id); }
    public void deleteServer(Long id) { serverRepository.deleteServer(id); }
}

6. REST API Controller

@RestController
@RequestMapping("/api/servers")
public class ServerController {
    @Autowired
    private ServerService serverService;
    @GetMapping
    public ResponseEntity<List<ServerConfig>> getServers() {
        return ResponseEntity.ok(serverService.getAllServers());
    }
    @PostMapping
    public ResponseEntity<Map<String, Object>> addServer(@RequestBody ServerConfig server) {
        try {
            Long id = serverService.saveServer(server);
            return ResponseEntity.ok(Map.of("success", true, "id", id));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
        }
    }
    @DeleteMapping("/{id}")
    public ResponseEntity<Map<String, Object>> deleteServer(@PathVariable Long id) {
        try {
            serverService.deleteServer(id);
            return ResponseEntity.ok(Map.of("success", true));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
        }
    }
    @PostMapping("/test")
    public ResponseEntity<Map<String, Object>> testConnection(@RequestBody ServerConfig server) {
        try {
            JSch jsch = new JSch();
            Session session = jsch.getSession(server.getUsername(), server.getHost(), server.getPort());
            session.setPassword(server.getPassword());
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect(5000);
            session.disconnect();
            return ResponseEntity.ok(Map.of("success", true, "message", "Connection test succeeded"));
        } catch (Exception e) {
            return ResponseEntity.ok(Map.of("success", false, "message", "Connection test failed: " + e.getMessage()));
        }
    }
}

7. Frontend Implementation

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Web SSH Client</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.css">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm-addon-fit.js"></script>
    <!-- Additional CSS omitted for brevity -->
</head>
<body>
    <!-- Sidebar, connection form, terminal container, file manager, modals, etc. -->
    <script src="js/webssh-multisession.js"></script>
</body>
</html>

8. Database Initialization

CREATE TABLE IF NOT EXISTS servers (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL COMMENT 'Server name',
    host VARCHAR(255) NOT NULL COMMENT 'Server address',
    port INT DEFAULT 22 COMMENT 'SSH port',
    username VARCHAR(100) NOT NULL COMMENT 'Username',
    password VARCHAR(500) NOT NULL COMMENT 'Password (store encrypted)',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert sample data
INSERT INTO servers (name, host, port, username, password) VALUES
('Local Test Server', 'localhost', 22, 'root', 'password'),
('Development Server', '192.168.1.100', 22, 'dev', 'devpass'),
('Test Server', '192.168.1.101', 22, 'test', 'testpass'),
('Production Server', '192.168.1.200', 22, 'prod', 'prodpass');

Performance Optimization & Best Practices

Use connection pooling, limit concurrent WebSocket sessions, cache server lists, and apply proper thread handling for SSH output streams.

Cache Optimization

@Service
@EnableCaching
public class CachedServerService {
    @Cacheable(value = "servers", key = "#username")
    public List<Server> getUserServers(String username) {
        return serverRepository.findByCreatedBy(username);
    }
    @CacheEvict(value = "servers", key = "#username")
    public void clearUserServersCache(String username) { }
}

Security Enhancements

@Component
public class SecurityEnhancements {
    public String encryptPassword(String password) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
            byte[] encrypted = cipher.doFinal(password.getBytes());
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("Password encryption failed", e);
        }
    }
    @EventListener
    public void handleSSHCommand(SSHCommandEvent event) {
        auditService.logSSHOperation(event.getUsername(), event.getServerHost(), event.getCommand(), event.getTimestamp());
    }
}

Conclusion

This article demonstrates how to build a functional Web SSH client with Spring Boot, WebSocket, and xterm.js, covering core architecture, code implementation, file transfer, security hardening, and performance tuning. The project serves as a practical example for learning real‑time WebSocket communication and SSH integration in Java.

Architecture diagram
Architecture diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

WebSocketxterm.jsWeb SSH
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.