一、背景及定义

        工作中需要经常使用SSH远程连接设备,每次都需要win+R,cmd,输入一长串ssh user@192.168.1.XX命令。想到可以做一个脚本工具,像应用那样双击即用,能快速连接到设备,无需过多输入,然后操作。

定义:SSH(Secure Shell)是一种网络协议和安全工具,用于在不安全的网络上安全地进行远程登录和数据传输。它提供了加密的通信通道,以保护敏感数据的机密性和完整性。

SSH协议支持多种应用,其中最常用的是SSH(远程登录)、SCP(Secure Copy Protocol)和SFTP(SSH File Transfer Protocol)。

二、主流SSH工具情况

        市面上常见的ssh工具有XshellPUTTYFinalshellMobaxtermVSCode等,这些工具都可用于连接,各有自己的优势和特长。

       1.Xshell

        Xshell易用性好,功能强大,可填写邮箱申请家庭和教育版。

       2. PUTTY

        PUTTY优点是小而轻巧,也很容易使用,只有几兆大小,虽然界面可能看起来一般。

       3.Finalshell

        Finalshell是国产的免费开源ssh工具,必需要支援一波,可以命令也可传输文件,第一次上手需要一丢丢时间,熟悉之后很好用。

       4. Mobaxterm

        Mobaxterm优点是可以用于设备的群体控制,简单意思讲就是可以多开,同时在多个终端输入相同内容Multipaste。

       5.VScode

        VScode优点是拓展性强,使用ssh连接需要安装插件,同时提一嘴,设备端也需要安装openssh-server。具体使用方法就不详细介绍了,可以参考以下链接中的内容使用。

VScode使用ssh远程连接Ubuntu【超简单且详细步骤】_vscode ssh-CSDN博客

        每种ssh工具都有自己的优势,而我想要的工具是:一方面能够快速连接远程设备,同时不需要过多的输入,前面网段在同一WIFI下一般不会变,也就是只输入最后一个数字就能连接,在断联后也能够快速重连。

三、SSH连接工具基本情况

1.基于python开发,部署在本地windows终端,使用pyinstaller封装,双击即用,补齐IP地址最后一段即可连接。

2.支持修改用户名和固定网段

3.断联后,支持按照原来的ip地址快速重连,或重新开始输入新的IP连接,或只打开普通的cmd终端,或关闭终端

4.第一次连接新设备也无需输入yes

四、使用步骤

1.双击SSH连接工具v1.0.2.exe

2.修改示例用户名和固定网段

(第一次使用需要修改示例用户名和固定网段)(后期使用不用每次修改)

示例用户名为abc,改为自己想连接的设备用户名,比如我想要远程的用户名为hhh

按1,输入新用户名,可以看到连接命令里的abc已经变为了hhh

按2修改固定网段,例如我的固定网段为192.168.77.XX

3.回车开始连接

补齐IP地址最后一段,例如我这里IP为102(可以设置设备开机后,自动通过企业微信群机器人播报自己的IP地址,参考doubao等AI助手)

输入密码

连接成功

##.连接断开

按1可以快速重连刚才的主机

按2,可以开始连接其他主机

按3,开启cmd终端

按q关闭终端

五、源代码

exe可执行文件地址https://wwbav.lanzouu.com/i7q6B3s4lktg    密码:h9mz

import json
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Dict, Optional


# ---------------------------------------------------------------------------
# 终端样式(ANSI)
# ---------------------------------------------------------------------------

class _T:
    RST = "\033[0m"
    BOLD = "\033[1m"
    CYAN = "\033[36m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    RED = "\033[31m"
    BLUE = "\033[34m"
    WHITE = "\033[97m"


def _enable_ansi_if_windows() -> None:
    if sys.platform != "win32":
        return
    try:
        import ctypes
        handle = ctypes.windll.kernel32.GetStdHandle(-11)
        mode = ctypes.c_uint32()
        if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(mode)) == 0:
            return
        new_mode = mode.value | 0x0004
        ctypes.windll.kernel32.SetConsoleMode(handle, new_mode)
    except Exception:
        pass


def _clear_screen() -> None:
    print("\033[3J\033[2J\033[H", end="", flush=True)


def _hr(char: str = "─", width: int = 65) -> None:
    print(f"{_T.WHITE}{char * width}{_T.RST}")


def _blank(n: int = 1) -> None:
    for _ in range(n):
        print()


def _title(text: str) -> None:
    _blank(1)
    print(f"{_T.BOLD}{_T.CYAN}{'═' * 65}{_T.RST}")
    print(f"{_T.BOLD}{_T.CYAN}  {text}{_T.RST}")
    print(f"{_T.BOLD}{_T.CYAN}{'═' * 65}{_T.RST}")
    _blank(1)


def _kv(label: str, value: str, indent: str = "  ") -> None:
    print(f"{indent}{_T.WHITE}{label}{_T.RST}  {_T.GREEN}{value}{_T.RST}")


def _menu_line(key: str, desc: str, indent: str = "  ") -> None:
    print(f"{indent}{_T.YELLOW}{key}{_T.RST}  {_T.WHITE}{desc}{_T.RST}")


def _hint(text: str, indent: str = "  ") -> None:
    print(f"{indent}{_T.WHITE}{text}{_T.RST}")


def _ok(text: str) -> None:
    print(f"  {_T.GREEN}{text}{_T.RST}")


def _warn(text: str) -> None:
    print(f"  {_T.YELLOW}{text}{_T.RST}")


def _err(text: str) -> None:
    print(f"  {_T.RED}{text}{_T.RST}")


def _prompt(text: str) -> str:
    print(f"{_T.BOLD}{_T.BLUE}{text}{_T.RST}", end="", flush=True)
    return input()


# ---------------------------------------------------------------------------
# 配置:EXE 兼容
# ---------------------------------------------------------------------------

DEFAULT_CONFIG = {
    "remote_user": "lab",
    "ip_prefix": "192.168.77.",
}


def get_config_path() -> Path:
    if getattr(sys, 'frozen', False):
        base = Path(sys.executable).parent
    else:
        base = Path(__file__).parent
    return base / "ssh_config.json"


def load_config() -> Dict[str, str]:
    cfg = DEFAULT_CONFIG.copy()
    path = get_config_path()
    if path.exists():
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
            for k in cfg:
                if isinstance(data.get(k), str) and data[k].strip():
                    cfg[k] = data[k].strip()
        except:
            pass
    return cfg


def save_config(cfg: dict) -> None:
    path = get_config_path()
    payload = {k: str(cfg.get(k, DEFAULT_CONFIG[k])).strip() for k in DEFAULT_CONFIG}
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


# ---------------------------------------------------------------------------
# 主逻辑
# ---------------------------------------------------------------------------

class RobotSSHConnector:
    def __init__(self):
        self.config = load_config()
        self.last_host = None
        self.last_user = None

    def _normalize_prefix(self):
        prefix = self.config["ip_prefix"]
        if prefix and not prefix.endswith("."):
            prefix = prefix + "."
        self.config["ip_prefix"] = prefix
        return prefix

    def _preview_command(self) -> str:
        user = self.config["remote_user"]
        prefix = self._normalize_prefix()
        return f"ssh -o StrictHostKeyChecking=accept-new {user}@{prefix}<主机号>"

    def settings_menu(self) -> None:
        while True:
            self._normalize_prefix()
            _title("SSH 连接工具 · 设置")

            _kv("远程用户名", self.config["remote_user"])
            _kv("IP 网段前缀", self.config["ip_prefix"])
            _blank(1)
            _kv("连接命令", self._preview_command())

            _blank(1)
            _hr()
            _menu_line("回车", "开始连接")
            _menu_line("1", "修改用户名")
            _menu_line("2", "修改网段")
            _hr()
            _blank(1)

            choice = _prompt("选择(1/2/回车):").strip()
            if choice == "":
                save_config(self.config)
                break
            if choice == "1":
                u = _prompt("新用户名:").strip()
                if u:
                    self.config["remote_user"] = u
                    save_config(self.config)
                    _ok("已保存")
                else:
                    _warn("未修改")
                time.sleep(0.4)
            elif choice == "2":
                p = _prompt("网段前缀(如 192.168.77.):").strip()
                if p:
                    if not p.endswith("."):
                        p += "."
                    self.config["ip_prefix"] = p
                    save_config(self.config)
                    _ok("已保存")
                else:
                    _warn("未修改")
                time.sleep(0.4)
            else:
                _warn("无效选项")
                time.sleep(0.4)

    def connect(self, user: str, host: str):
        self.last_user = user
        self.last_host = host

        target = f"{user}@{host}"
        cmd = ["ssh", "-o", "StrictHostKeyChecking=accept-new", target]

        print(_T.RST, end="", flush=True)
        _clear_screen()

        try:
            code = subprocess.call(cmd)
            _blank(2)
            _hint(f"✅ SSH 会话已断开,退出码:{code}")
        except FileNotFoundError:
            _err("❌ 未找到 ssh 命令,请安装 OpenSSH 客户端")
        except Exception as e:
            _err(f"❌ 连接异常:{e}")

    def _new_cmd_tab_in_current_terminal(self):
        """在当前 Windows Terminal 新开 CMD 标签页;普通 CMD 则 fallback 新窗口"""
        if sys.platform != "win32":
            subprocess.Popen("bash", shell=True)
            return

        # 优先用 wt.exe 在当前窗口新开 CMD 标签页
        try:
            # wt.exe 是 Windows Terminal;-p 后面是你的 CMD profile 名
            subprocess.Popen(
                ["wt.exe", "new-tab", "-p", "命令提示符"],
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            _ok("✅ 已在当前终端打开新 CMD 标签页")
            return
        except Exception:
            pass

        # 没有 Windows Terminal 时,fallback 到独立 CMD 窗口
        try:
            subprocess.Popen("cmd.exe", creationflags=subprocess.CREATE_NEW_CONSOLE)
            _ok("✅ 已打开独立 CMD 窗口(未检测到 Windows Terminal)")
        except Exception as e:
            _err(f"❌ 打开终端失败:{e}")

    def show_reconnect_menu(self):
        while True:
            _blank(1)
            _hr("=", 65)
            _hint("连接已断开,请选择操作:")
            _menu_line("1", "重新快速连接(直接连刚才的主机)")
            _menu_line("2", "开始新的连接(回到设置页面)")
            _menu_line("3", "打开新的CMD标签页")
            _menu_line("q", "关闭窗口退出")
            _hr("=", 65)
            _blank(1)

            choice = _prompt("请输入选择 [1/2/3/q]:").strip().lower()

            if choice == "1":
                if self.last_host and self.last_user:
                    _ok(f"✅ 正在重新连接:{self.last_user}@{self.last_host}")
                    self.connect(self.last_user, self.last_host)
                else:
                    _warn("❌ 无最近连接记录,无法重连")
                    time.sleep(1)
            elif choice == "2":
                _ok("✅ 返回设置页面,开始新连接")
                time.sleep(0.8)
                self.settings_menu()
                self.connect_in_this_terminal()
                break
            elif choice == "3":
                self._new_cmd_tab_in_current_terminal()
                time.sleep(1)
            elif choice == "q":
                _ok("👋 正在退出...")
                break
            else:
                _warn("⚠️ 无效输入,请输入 1/2/3/q")
                time.sleep(0.5)

    def connect_in_this_terminal(self) -> None:
        prefix = self._normalize_prefix()
        user = self.config["remote_user"]

        _title("连接主机")
        _kv("网段", prefix)
        _hint("输入主机号,或完整IP")
        _hr()
        _blank(1)

        hint = _prompt("主机号/IP:").strip()
        if not hint:
            _warn("已取消")
            return

        host = hint if "." in hint else f"{prefix}{hint}"
        self.connect(user, host)
        self.show_reconnect_menu()


def run() -> None:
    _enable_ansi_if_windows()
    try:
        app = RobotSSHConnector()
        app.settings_menu()
        app.connect_in_this_terminal()
    except KeyboardInterrupt:
        _warn("🛑 已退出")
        time.sleep(0.8)
    except Exception as e:
        _err(f"错误:{e}")
        input("按回车关闭...")


if __name__ == "__main__":
    run()

六、错误修复

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                     
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @                                                             
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                                                             
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!                                                                   
Someone could be eavesdropping on you right now (man-in-the-middle attack)!                                             
It is also possible that a host key has just been changed.

问题原因
机器人工控机重装系统、重装 ssh 服务后,主机密钥变了。你本地known_hosts里还保存着旧密钥,SSH 触发安全拦截,直接连接失败。

当前代码里写了:
"-o", "StrictHostKeyChecking=accept-new"
这个参数只允许新增主机自动保存密钥,不允许覆盖已存在的旧密钥,所以依然会报错。

解决方法

清除旧指纹

ssh-keygen -R 192.168.XX.XX

然后再重新运行

更多推荐