凌晨 3 点 02 分。
告警群第 14 条消息弹出来的时候,我人在郊区的民宿,手机连着房东的 WiFi,VPN 客户端右下角的小图标在转着圈,已经转了 40 秒。
跳板机的二次验证码 30 秒一换,等我点开 Authenticator,VPN 终于连上,但 SSH 还是没握上手。这种时候哪怕你是 10 年老 Java,也只能干瞪眼。
我打开浏览器,登进我们自己那套基于 erupt 写的后台管理系统------它本来就是要 7×24 开着的,HTTPS、域名、SSO 全配齐了。在菜单栏点了一下"Terminal",新开的一个浏览器 Tab 里,光标在闪。
lua
Last login: Wed May 27 03:02:14
▶ ERUPT v1.14.3 Low-Code · Zero-Frontend · Open Source
Host my-server OS Linux 5.15.0 amd64 Java 17.0.9
[root@my-server ~]$ tail -f /var/log/app/error.log
10 分钟后我躺回床上。

一、为什么"在浏览器里跑终端"这件事,今年突然变得重要
过去十年我们都习惯了一套工作流:服务器在内网 → 跳板机做 ACL → 本地装个 XShell / iTerm / Termius / Tabby → SSH 上去敲命令。
但 2026 年的几个新现实,正在把这套链路一点点压垮:
第一,越来越多团队没有"固定研发机" 。客户实施同学、PM、运维、SaaS 上的二级管理员------他们要看日志、跑临时脚本、改个 nginx 配置,但只有后台账号,没有 SSH 权限。每次出问题就排队找研发,研发要一遍遍 paste 命令,效率低还容易出事。
第二,跳板机/堡垒机过门越来越烦。VPN → 跳板机 → 二次验证 → 选目标机 → 跳上去------4 道门,每道都可能掉链子。手机端尤其灾难,外网应急的时候让人想砸键盘。
第三,合规要求"所有命令操作必须留痕"。SSH 客户端是装在个人电脑上的,命令历史散落各处,做安全审计基本拿不到全量记录。后台管理系统反而是天然有审计能力的入口。
第四,AI 时代的运维范式在变 。让 AI Agent 去跑命令、看日志、改配置,它显然没法手动开 XShell。它需要一个有鉴权、有边界、可被程序调用的 shell 入口。
社区其实早就嗅到这个方向------阿里云开发者社区那篇《再见 xShell,自己用 Java 撸一个 Web 版》两年多还在被转发,V2EX 上 Termix(自托管 SSH 管理工具)的发布帖去年九月顶到了热门,xtermjs-spring-boot-starter 也在 Maven 中央仓静静躺了好几年。这不是新需求,但一直没有"开箱即用 + 接生产权限"的成品。
直到这个月发布的 erupt-terminal。
二、一行依赖,菜单自动出现
很多"Web SSH"项目,README 第一句话就是"先 npm install xterm 然后启动 node-pty 服务再 fork 一个进程......"。erupt-terminal 不一样,它假定你已经在用 erupt 写后台------所有该有的东西(登录、菜单、角色、HTTPS)已经在那儿了。
步骤一:
xml
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-terminal</artifactId>
<version>1.14.3</version>
</dependency>
步骤二:......没有步骤二了。
mvn install 完,重启服务,左侧菜单自动多出一个图标是 fa fa-terminal 的 Terminal 入口。点进去就是终端。
为什么能做到?因为 erupt 的每个模块都是一个 EruptModule:
java
@Configuration @EruptScan @EnableWebSocket
public class EruptTerminalAutoConfiguration implements EruptModule {
public static final String TERMINAL_KEY = "erupt-terminal.html";
@Override
public List<MetaMenu> initMenus() {
MetaMenu m = MetaMenu.createSimpleMenu(
"terminal", "Terminal", TERMINAL_KEY, null, 120, EruptTplService.TPL);
m.setIcon("fa fa-terminal");
return List.of(m);
}
}
启动时框架扫到这个模块,把菜单写进 e_upms_menu 表,前端读菜单自动渲染------RBAC、i18n、图标、权限分配全部自动接上。这就是 erupt 的模块化设计在这种场景下的红利:你不是"集成"了一个 web terminal,你是给后台系统装了一个内置功能。
三、能力盘点:为什么这个不是又一个 Demo
我把市面上几个流行的 web terminal 方案放在一起对比过。先说结论:erupt-terminal 是少数直接可以接进生产环境的一个。逐条说原因。
1. 真 PTY,不是 ProcessBuilder
很多 Web Shell 项目用 Java 的 Runtime.exec 或 ProcessBuilder 起进程,结果就是 vim 进去界面错乱、top 不刷新、htop 直接报错、Ctrl-C 杀不掉、Tab 补全没反应。
erupt-terminal 的内核是 JetBrains 开源的 pty4j(IntelliJ 系列 IDE 内置终端用的就是它)。它在 Unix 上调 forkpty(3),在 Windows 上对接 winpty/ConPTY,给到的是一个真正的伪终端 。设个 TERM=xterm-256color,再开个 COLORTERM=truecolor,颜色、光标、字符宽度计算都对了。
2. 多标签 + Alt+T 新开
一个 Tab 跑日志,一个 Tab 跑 kubectl,一个 Tab 跑临时 SQL------三个 PtyProcess 互不干扰,状态完全隔离。Alt+T 直接开新页。代码就在模板里 22 行 JS:
javascript
document.addEventListener('keydown', function (e) {
if (e.altKey && e.key === 't') { e.preventDefault(); createTab(); }
});
3. 自动重连 + 心跳 + 空闲超时
WebSocket 最怕"信号断了你不知道"。erupt-terminal 在这一块写得比预期细致:
- 服务端每 30 秒 ping 一次所有活跃 session,断连立刻可感知
- 客户端最多重试 8 次,1s → 指数退避 ,重连成功后会打印一行
── reconnected ── - 30 分钟无操作自动断开,释放服务器资源(这条避免了上线后被滥用变堡垒机)
java
// TerminalEndpoint.java(节选)
private static final long IDLE_TIMEOUT_MS = 30 * 60 * 1000L;
idleChecker.scheduleAtFixedRate(TerminalEndpoint::checkIdle,
1, 1, TimeUnit.MINUTES);
idleChecker.scheduleAtFixedRate(TerminalEndpoint::pingAll,
30, 30, TimeUnit.SECONDS);
4. Token + 菜单权限双重校验
打开 WebSocket 连接的第一件事,不是 fork shell,而是验证:
java
List<String> tokens = session.getRequestParameterMap()
.get(EruptMutualConst.TOKEN);
if (tokens == null || !tokenService.tokenExist(tokens.get(0))) {
session.close(new CloseReason(PROTOCOL_ERROR, "Unauthorized"));
return;
}
if (userService.getEruptMenuByValue(TERMINAL_KEY, token) == null) {
session.close(new CloseReason(VIOLATED_POLICY, "Forbidden"));
return;
}
有效的 erupt-token + 用户角色绑定了 Terminal 菜单------两条都满足才放行。这意味着:
- 给客户实施的角色,可以只授权"日志查看"菜单 + Terminal,他能看日志但碰不到用户表
- 给运维的角色,授权 Terminal + 部分配置类菜单
- 普通业务用户角色,根本看不到 Terminal 入口
不用再为"web shell 不安全"焦虑,因为它复用了你已经审过、跑了几年的 RBAC。
5. 跨平台
Linux/macOS 起 $SHELL(兜底 /bin/bash),Windows 起 cmd.exe。开发机 Mac、测试机 Linux、客户现场 Windows------同一份 jar 跑下来都是一样的体验。
6. 优雅的视觉细节
JetBrains Mono 字体内置在 jar 里,连接成功后的横幅是手写的 ANSI 序列:
css
▶ ERUPT v1.14.3 Low-Code · Zero-Frontend · Open Source
Host my-server OS Linux 5.15.0 amd64 Java 17.0.9
紫色品牌色 + 灰色辅助文字 + 黄色高亮主机信息。这一段是设计同学单独抠过的------一个产品级 web terminal 该有的样子。
四、给"AI 时代"留了一道门
注意,erupt 1.14.3 同期还更新了另一个模块:erupt-ai-claw 新增了 enableExecShell 配置项。
这两件事放在一起看就有意思了------你既给"人"准备了一个浏览器终端,也给"AI Agent"准备了一个可控的 Shell 调用入口,两者共享同一套 RBAC。
意思是:未来你可以让一个 AI Agent 只在"运维角色"下被允许调用 Shell,并且每次调用都可以在审计日志里留痕。这是单纯一个 web ssh starter 给不了的整体设计。
五、什么场景下你会真的用上它
挑几个真实场景照镜子:
- 2B SaaS 平台:客户内部部署,运维不会用 SSH,但能登后台------给运维角色加 Terminal,客户工程师 0 培训成本
- 多租户管理后台:每个租户的二级管理员,按角色精细控制能不能开终端
- 客户现场实施 :堡垒机审批要 2 小时,浏览器后台 30 秒就能
tail -f - 手机应急:iPhone 自带的 Safari 也能跑(xterm.js 支持触屏),可以临时救火
- K8s 容器内自管理 :把 erupt 应用本身打进容器,浏览器开终端就等于
kubectl exec进了这个 pod - 教学 / 演示场景:开个公网 demo 站点,让学生在浏览器里练命令,省掉每人开 EC2
六、用之前,请把这两件事记在脑子里
1. 终端进程权限 = 运行 Java 的系统用户权限。
如果你用 root 跑 Spring Boot,那 web 终端就是 root shell------这跟你给那个用户开 SSH 是一样的安全模型。生产环境强烈建议用专用 OS 用户跑 Java,并把 Terminal 菜单只授权给受信任的管理员角色。
2. 生产必须 HTTPS / WSS。
WebSocket over HTTP 等于明文传命令。这件事框架不会替你做,是基础设施侧的责任。配 nginx / ingress 时把 Upgrade 和 Connection 头透传好。
文档里也明明白白写了这条警告,不是我吓唬人。
七、写在最后
erupt 这两年最让我意外的,是它从一个"@Erupt 注解生成增删改查 UI"的低代码框架,正在变成一个完整的 Java 后台管理操作系统:
- 菜单、用户、角色、权限------
erupt-upms - 工作流------
erupt-flow - BI 分析------
erupt-cube - AI 集成 + Agent 协作 + 跨会话记忆------
erupt-ai/erupt-ai-claw - 现在再加一个浏览器终端------
erupt-terminal
每个模块都是一行 Maven 依赖,自动注册菜单,自动接入鉴权。这种"乐高式"的扩展能力,在 Spring Boot 生态里其实非常少见。
如果你今晚刚好在线,0:00 到 1:00,正好可以把 erupt-terminal 加进你那个用了 erupt 的内部系统试一下,亲手摸一次"在浏览器里 tail -f 生产日志"是什么手感。
🌟 项目地址:
https://github.com/erupts/erupt--- 顺手点个 Star,这是开源作者最大的动力来源