Build a WebSSH Terminal with Spring Boot, WebSocket and xterm.js
This article walks through creating a WebSSH solution from scratch using Spring Boot, WebSocket, JSch and the xterm.js front‑end library, covering dependency setup, backend WebSocket configuration, message handling, SSH session management, and a minimal HTML/JS client to display a browser‑based terminal.
Introduction
To meet a project requirement for a web‑based SSH terminal, the author decided to implement a custom WebSSH service instead of using existing Python‑based tools that would add heavy dependencies to the server.
Technology Selection
Because WebSSH needs real‑time data exchange, a long‑lived WebSocket connection is used. The stack chosen is Spring Boot for the backend, JSch for SSH handling, and xterm.js for the front‑end terminal UI.
Dependency Import
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
</parent>
<dependencies>
<!-- Web related -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JSch for SSH -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.54</version>
</dependency>
<!-- WebSocket support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- File upload utilities -->
<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>Simple xterm.js Example
<!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>The page renders a shell‑like interface, which serves as the foundation for the full WebSSH implementation.
Backend Implementation
WebSocket Configuration
@Configuration
@EnableWebSocket
public class WebSSHWebSocketConfig implements WebSocketConfigurer {
@Autowired
WebSSHWebSocketHandler webSSHWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSSHWebSocketHandler, "/webssh")
.addInterceptors(new WebSocketInterceptor())
.setAllowedOrigins("*");
}
}Interceptor
public class WebSocketInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
String uuid = UUID.randomUUID().toString().replace("-", "");
attributes.put(ConstantPool.USER_UUID_KEY, uuid);
return true;
}
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {}
}Handler
@Component
public class WebSSHWebSocketHandler implements WebSocketHandler {
@Autowired
private WebSSHService webSSHService;
private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("User:{} connected to WebSSH", session.getAttributes().get(ConstantPool.USER_UUID_KEY));
webSSHService.initConnection(session);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
if (message instanceof TextMessage) {
logger.info("User:{} sent command:{}",
session.getAttributes().get(ConstantPool.USER_UUID_KEY), message.toString());
webSSHService.recvHandle(((TextMessage) message).getPayload(), session);
} else {
System.out.println("Unexpected WebSocket message type: " + message);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
logger.error("Data transport error");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
logger.info("User:{} disconnected", session.getAttributes().get(ConstantPool.USER_UUID_KEY));
webSSHService.close(session);
}
@Override
public boolean supportsPartialMessages() { return false; }
}Service Interface
public interface WebSSHService {
void initConnection(WebSocketSession session);
void recvHandle(String buffer, WebSocketSession session);
void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;
void close(WebSocketSession session);
}Core Business Logic (excerpt)
public void initConnection(WebSocketSession session) {
JSch jSch = new JSch();
SSHConnectInfo info = new SSHConnectInfo();
info.setjSch(jSch);
info.setWebSocketSession(session);
String uuid = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
sshMap.put(uuid, info);
}
public void recvHandle(String buffer, WebSocketSession session) {
ObjectMapper mapper = new ObjectMapper();
WebSSHData data = null;
try {
data = mapper.readValue(buffer, WebSSHData.class);
} catch (IOException e) {
logger.error("JSON conversion error: {}", e.getMessage());
return;
}
String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(data.getOperate())) {
SSHConnectInfo info = (SSHConnectInfo) sshMap.get(userId);
executorService.execute(() -> {
try {
connectToSSH(info, data, session);
} catch (Exception e) {
logger.error("WebSSH connection error: {}", e.getMessage());
close(session);
}
});
} else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(data.getOperate())) {
String command = data.getCommand();
SSHConnectInfo info = (SSHConnectInfo) sshMap.get(userId);
if (info != null) {
try {
transToSSH(info.getChannel(), command);
} catch (IOException e) {
logger.error("WebSSH command error: {}", e.getMessage());
close(session);
}
} else {
logger.error("Unsupported operation");
close(session);
}
}
}
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
session.sendMessage(new TextMessage(buffer));
}
public void close(WebSocketSession session) {
String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
SSHConnectInfo info = (SSHConnectInfo) sshMap.get(userId);
if (info != null) {
if (info.getChannel() != null) info.getChannel().disconnect();
sshMap.remove(userId);
}
}Front‑End Implementation
HTML Page
<!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>JavaScript Client
function openTerminal(options) {
var client = new WSSHClient();
var term = new Terminal({
cols: 97,
rows: 37,
cursorBlink: true,
cursorStyle: "block",
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 + '
'); },
onConnect: function() { client.sendInitData(options); },
onClose: function() { term.write('\rconnection closed'); },
onData: function(data) { term.write(data); }
});
}
openTerminal({
operate: 'connect',
host: 'IP_ADDRESS',
port: 'PORT',
username: 'USERNAME',
password: 'PASSWORD'
});Demo Screenshots
Conclusion
The article demonstrates a complete, dependency‑light WebSSH project built entirely with Java and Spring Boot on the backend and xterm.js on the front‑end, and mentions possible extensions such as file upload/download.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
