CLAUDE CODE生成可视化数据库工具

被堡垒机封了端口转发?把 SSH 终端通道封成 MySQL 可视化客户端

一个被 JumpServer 协议层封死端口转发的合规内网,最终用 paramiko + Flask 在不绕开授权的前提下给运维和开发提供了一套"准 Navicat"体验。本文复盘从踩坑到全栈实现的关键技术细节。

一、痛点:堡垒机协议层封了端口转发,下游全死

公司用的是市面上最常见的 JumpServer 堡垒机,登录路径是这样的:

markdown 复制代码
本地 → 堡垒机域名:端口
       → 输动态码(keyboard-interactive MFA,TOTP)
       → 在堡垒机 shell 里输服务器 ID
       → 跳转到目标内网机器
       → sudo su / 执行业务命令

数据库则散布在内网两类机器上:

  • 每台业务机本机自带 MySQL(分片库,业务数据要按分片查)
  • 集中 DB 机 一台,放公共业务库 A / B / C / D 等

合规要求所有 DB 访问都必须走堡垒机。痛点开始浮现:

  • Navicat / Workbench / DBeaver 的 SSH 隧道全部走不通;
  • ssh -L 13306:127.0.0.1:3306 <jump> 也走不通;
  • 哪怕用 ProxyJump 二段链,依然走不通。

报错全是一句:

arduino 复制代码
channel 3: open failed: administratively prohibited: direct-tcpip not support

关键事实 :JumpServer 在协议层 禁用了 direct-tcpip 通道。这是合规要求,不是网络配置------别再试隧道方案了,整条路是死的。

二、思路换轨:放弃端口转发,复用终端通道

走不通隧道,那就老老实实走终端:

css 复制代码
本地 paramiko → 跳板机 PTY → 跳转目标机 → 跑 mysql --batch → 抓文本 → 本地解析

这个思路有几个看似不起眼但要命的细节:

  1. MFA 不能让用户每次输一遍------浏览器点一个 SQL 就要输一次动态码,谁忍得了?
  2. mysql 命令在很多机器上不在 PATH (业务机往往装在自定义路径,比如 /data/mysql/bin/mysql);
  3. SQL 里的引号、反引号、换行塞进 shell 命令会被双重转义吃掉;
  4. 密码不能进 argv ,否则 ps -ef 一抓就泄露;
  5. 多个并发查询不能互相串扰,且不希望每次都重新认证;
  6. PTY 终端输出夹杂着命令回显、shell prompt、ANSI 转义码,得把"真正的查询结果"从这一坨里挑出来。

把这些坑挨个填了,就有了一套相对稳的方案。

三、核心设计:单 Transport + 多 PTY Channel

paramiko 的设计哲学很对胃口:一个已认证的 Transport(TCP 长连接 + 加密会话)上可以开任意多个独立的 channel,每个 channel 各自跑一个 PTY shell。

python 复制代码
class JumpHostSession:
    """跳板机的持久 Transport,全程只认证一次。"""

    def connect(self):
        sock = socket.create_connection((host, port), timeout=self._timeout)
        self._transport = paramiko.Transport(sock)
        self._transport.connect()
        # 密码 + keyboard-interactive (MFA) 二段认证
        remaining = self._transport.auth_password(username, password)
        if remaining:
            self._transport.auth_interactive(username, self._mfa_handler)

    def open_worker(self) -> "JumpWorker":
        """同一 transport 上开一个独立 PTY shell,可并发开 N 个。"""
        channel = self._transport.open_session()
        channel.get_pty(term="xterm", width=220, height=50)
        channel.invoke_shell()
        return JumpWorker(channel, self._timeout)

调用关系长这样:

markdown 复制代码
JumpHostSession       ── 跳板机持久 Transport(只认证一次)
  └─ JumpWorker       ── 同一 Transport 上的独立 PTY shell,可并发多个
       └─ ServerSession ── 单台目标机会话(context manager)

这个结构的好处:

  • 认证一次,全程复用:动态码只在工具启动时输一次,浏览器后续点 SQL 不再 prompt;
  • 并发不串台:每个 worker 独占自己的 channel 和读缓冲;
  • 跨机器查询可以缓存 worker:把"已经跳转进 ssh_host A 的那个 worker"留着,下次查 A 上的 DB 不重新跳。

四、关键技术点(真干货)

1. MFA 自动化:TOTP 种子 + auth_interactive

主流堡垒机的动态码本质是 RFC 6238 的 TOTP(常见 30 秒周期,部分企业方案配 60 秒)。paramikoauth_interactive 接受一个 prompt handler,由我们填回应:

python 复制代码
def _mfa_handler(self, title, instructions, prompts):
    responses = []
    for prompt, echo in prompts:
        if self._totp:                          # 配了 totp_secret
            responses.append(self._totp.now())
        else:                                   # 没配 → 终端手输
            responses.append(input(f"\n[动态码] {prompt.strip()} ").strip())
    return responses

totp_secret 仅落在本地 config.json,文件入 _secrets/ 子目录且 git 忽略。

2. keep-alive 心跳,避免会话被堡垒机 idle kill

很多堡垒机 idle 5 分钟就掐连接,本机开着 Web 界面但用户中午吃饭去了------回来一刷 SQL 全报"connection lost"。

paramiko 提供了 SSH IGNORE 包,作为低开销心跳:

python 复制代码
def start_keepalive(self, interval: int = 30):
    def _ka():
        while not self._ka_stop.wait(interval):
            if self._transport and self._transport.is_active():
                self._transport.send_ignore()
    self._ka_thread = threading.Thread(target=_ka, daemon=True, name="ssh-keepalive")
    self._ka_thread.start()

注意 daemon=True------主进程退出时心跳线程跟着死,不留僵尸。

3. marker 包裹 + ANSI 剥离:从 PTY 噪音里抽结果

PTY 输出长这样(缩简版):

ruby 复制代码
\x1b[?2004h[root@db ~]# echo X3f7a2; mysql -uadmin ...; echo Y8d1c4:$?
X3f7a2
id   name
1    Alice
2    Bob
Y8d1c4:0
\x1b[?2004l[root@db ~]#

里面同时混着:命令回显、shell prompt、ANSI 控制序列、真正的查询结果、退出码。

解法是用随机 marker 把真正的输出夹起来

python 复制代码
def _exec(self, w, cmd: str, timeout: int = 120):
    start = "X" + uuid.uuid4().hex[:10]
    end   = "Y" + uuid.uuid4().hex[:10]
    w._send(f"echo {start}; {cmd}; echo {end}:$?\n")
    raw = _strip_ansi(w._read_until(rf"{re.escape(end)}:\d", timeout=timeout))
    si = raw.rfind(start)
    ei = raw.rfind(end)
    code = int(re.search(rf"{re.escape(end)}:(\d+)", raw[ei:]).group(1))
    return code, raw[si + len(start):ei].strip("\n")

uuid 保证 marker 不会和数据撞车,rfind 而不是 find 是为了对付命令本身也回显了 marker 字符串的情况------只有最后一对 marker 才是真分隔符。

ANSI 转义码用一条经过实战验证的正则吃掉:

python 复制代码
_ANSI_RE = re.compile(r'\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')

4. base64 转送 SQL:彻底躲过 shell 转义地狱

用户在 Web 上敲的 SQL 可能长这样:

sql 复制代码
SELECT id, JSON_EXTRACT(data, '$.items[*].id') AS items
FROM t_user
WHERE nickname LIKE '%admin\'s alt%' AND created_at > '2026-01-01';

里面有单引号、双引号、反斜杠、$*%------任何一个塞进 mysql -e "..." 都得二次转义,错一个就废。

解法简单粗暴:本地 base64 编码,远端解码后管道喂给 mysql

python 复制代码
sql_b64 = base64.b64encode(sql.encode("utf-8")).decode("ascii")
mysql_cmd = (f"echo {sql_b64} | base64 -d | "
             f"{mysql_bin} -h{host} -P{port} -u{user} {db_arg} "
             f"--batch --column-names 2>&1")

--batch 让 mysql 输出 tab 分隔(机器可读),--column-names 第一行是列名。

5. MYSQL_PWD:密码不进 argv

mysql -p"$pwd" 是错的------ps -ef 任何同机用户都能看到完整密码。正解是用环境变量:

python 复制代码
w._send(f"export MYSQL_PWD='{pw}'\n")

export 完的环境变量只活在这个 worker 的 shell 进程里,关闭 worker 即销毁。

6. mysql 客户端路径探测

业务机为了不让 PATH 污染应用环境,常把 mysql 客户端塞在自定义路径(比如 /data/mysql/bin/mysql);集中 DB 机则一般在 /usr/bin/mysql。直接 mysql 就会 command not found,且这个错误信息会被新人误判成"机器没装 mysql"。

按优先级探测,找到为止:

python 复制代码
_MYSQL_PATHS = ["/data/mysql/bin/mysql", "/usr/local/mysql/bin/mysql",
                "/usr/bin/mysql", "/usr/local/bin/mysql"]

def _detect_mysql(self, w) -> str:
    cands = " ".join(_MYSQL_PATHS)
    code, out = self._exec(
        w, f"command -v mysql 2>/dev/null || ls {cands} 2>/dev/null | head -1")
    return out.strip().split("\n")[0].strip() or "mysql"

探到的路径按 ssh_host 缓存,第二次查同一机器直接命中,省一轮往返。

7. sudo 智能降级:免密则切、要密码则取消

部分机器需要 root 才能跑 mysql(账户文件 600 给 root)。但有的账户配了 NOPASSWD,有的没有。脚本里既不能死等密码 prompt 把整个 worker 卡死,也不能一上来就把所有用户当无 sudo:

python 复制代码
def _enter_root(self, w):
    w._send("sudo su\n")
    got = _strip_ansi(w._read_until(r"#\s*$|[Pp]assword", timeout=8))
    if re.search(r"[Pp]assword", got):
        w._send("\x03")     # Ctrl-C 取消 sudo password prompt
        w._read_until(_PROMPT, timeout=5)
    # else: 顺利切到 root(提示符已经变成 `#`)

\x03 是 Ctrl-C 的字节序列,在 PTY 里等价于真按了一下。

五、上层:Flask + 模板 = 浏览器可视化

底层 DbSession.query(sql, ssh_host, database) 返回的是 {columns, rows, rowcount, error} 字典,套个 Flask 就是 Web 客户端:

arduino 复制代码
启动 python db_web.py    → 终端输一次动态码
浏览器 http://127.0.0.1:5000
├── 左侧:库表树(点表名出 schema)
├── 中间:SQL 编辑器 + 历史
├── 右侧:结果表格(前端分页 + 排序)
└── 顶栏:server_host 下拉 / 只读开关 / 导出 CSV·Excel

界面没什么花活,关键是调用链稳 :浏览器 fetch → Flask 路由 → DbSession.query → paramiko worker → mysql → tab 解析 → JSON 返回。整链路 < 300ms,比 Navicat 慢一点点(Navicat 走 native 协议),但合规、不耗第二张令牌。

六、复盘

回看整个工具,最大的省力点不是 Web 界面,而是承认"端口转发死路一条"早------前面几年陆陆续续有同事试过 sshuttle、autossh、frp 反代,没一个走通。承认这一点之后,问题立刻从"怎么打通隧道"退化成"怎么从一个交互式 PTY 里精确抽出结构化数据"------这是个有成熟解法的工程问题。

技术栈也很朴素:

  • paramiko:SSH 客户端,绕开 git bash MSYS ssh 在 PowerShell 里 TTY 卡死的坑;
  • pyotp:TOTP 自动生成;
  • Flask + Jinja2:最薄的 Web 层;
  • 一条 ANSI 正则 + 一对 marker:解析 PTY 噪音。

没用任何 ORM、没装任何数据库 driver------所有结构化全来自 mysql --batch 的 tab 输出 + 一个手写 unescape:

python 复制代码
def _unescape(s: str) -> str:
    """Reverse mysql --batch escaping (\\t \\n \\0 \\\\)."""
    out, i = [], 0
    while i < len(s):
        if s[i] == "\\" and i + 1 < len(s):
            out.append({"t": "\t", "n": "\n", "0": "\0", "\\": "\\"}.get(s[i+1], s[i+1]))
            i += 2
        else:
            out.append(s[i]); i += 1
    return "".join(out)

写工具的快慢,往往不取决于你写了多快,而取决于你多快放弃错的方向。


文中工具是个人产能工具,未开源;如果你也在被堡垒机封掉的内网里挣扎,希望这套思路能省你几个小时。

相关推荐
星星电灯猴2 小时前
全面解决Charles抓取HTTPS请求响应中文乱码问题的方法与技巧
后端·ios
道友可好2 小时前
写给 AI 的入职手册,AGENTS.md
前端·人工智能·后端
sandnes3 小时前
把ToolUse循环做到生产级-错误处理与可靠性五件套
后端
掘金者阿豪3 小时前
全维度拆解具身智能:底层技术 + 实战落地 + 全球产业竞争
后端
秋天的一阵风3 小时前
✨ 代码秒跳转、自动补全?全靠 LSP 和 AST!
前端·后端·ai编程
用户298698530143 小时前
Java 中的 HTML 解析:从文件读取、URL 抓取到数据提取
java·后端
AskHarries3 小时前
ZJF.AI:简单、稳定、免费的图片托管与外链分享平台
后端
百珏3 小时前
流量没暴涨,网关却挂了:Spring Cloud Gateway 从 500 QPS 优化到 4200 QPS
后端·spring cloud·架构
ICT系统集成阿祥3 小时前
什么是AI ECN?
后端