笔者由于工作需要,经常会使用SSH和SCP命令,进行相关远程系统的连接、操作和文件传输。 我们都知道,由于一些安全上的考虑,是不能在ssh连接命令中,直接输入密码作为参数的,只能在交互过程中使用,如果这个操作非常频繁,或者输入不变(密码非常复杂),或者希望通过程序来完成这些任务来实现自动化,就会受到一些限制,并带来一些不变。
传统的解决方案就是:copy-id。我们先来简单的复习一下这个过程。
SSH Copy ID (公钥复制)
SSH CopyID实现无交互登录的本质,其实是使用公钥登录,而非自动输入密码。它的配置过程和登录的原理如下:
- 首先在客户端,需要使用ssh-keygen命令,在客户端系统当前用户环境中,生成一对公私钥文件,其中公钥文件通常为id_rsa.pub
- 然后,在客户端系统中,使用ssh-copy-id命令,连接SSH服务端系统,并将客户端用户的公钥复制到服务端
- 这一次连接是需要提供服务端使用的用户名和登录密码的
- 连接成功后,提供的公钥,会保存在服务端用户主目录的authorized_keys文件中
- 如果整个操作成功,在后续的连接中(使用ssh或者scp),就不需要再次提供用户密码了
- 客户端使用ssh和用户名连接服务端
- 服务端向登录请求,发送一个随机的挑战信息
- 客户端会使用自己的私钥,对挑战信息进行签名,然后发送给服务端
- 服务端会基于登录请求的用户,使用其主目录下保存的公钥信息,对签名进行验证
- 验证通过,服务端确认当前登录拥有匹配的私钥,从而确认认证完成并建立SSH连接会话
所以,这个过程的本质是,生成一个非对称密钥对,在客户端的用户环境中,保存私钥;在服务端的用户环境中,保存其匹配的公钥;认证过程就是对随机信息的签名和验证;从而密码学机制,确认客户端当前这个用户和服务端用户的逻辑关联。这个机制认为用户的私钥,是在用户环境中予以保护的,就不需要额外的服务端用户密码了。
CopyID确实能够解决登录过程无需用户交互过程的需求,使用也比较方便,也能够保障这一过程的安全。但这个配置过程略显繁琐,有没有其他无需交互式输入密码建立连接的技术方案呢?
笔者在研究中发现了一种基于expect命令工具的连接方案。觉得这个东西还有点意思,想要分享给读者。
expect
简单而言,expect是一个命令行工具,它可以方便的处理命令行环境中的操作指令和信息,来模拟人机交互过程。expect的英文原意是"期望"的意思,大概的意思可能就是这个程序可以基于预期的文本输出来匹配对应的动作执行,从而实现模拟人类用户和交互式程序直接的交互方式吧。
这样的表述比较抽象,我们先来看一个实际应用的案例。笔者想编写一个脚本命令,可以将当前文件夹中的所有文件文件使用scp命令,复制到备份服务器上,然后移动到另一个本地文件夹中。由于以后可能需要使用程序调用来实现自动化,所以不能在中间进行交互(如输入密码),并且这个指令可以参数化以方便配置和调用。
经过研究,笔者发现可以通过expect来实现,具体过程如下。
安装
首先需要先安装expect命令,这里确实令人遗憾,这个命令并不是原生命令。但通常可以直接安装。安装完成后,最好检查一下其安装位置:
sudo apt install expect
which expect
正常默认情况下,expect会安装到 /usr/bin文件夹中,可以在系统范围内直接使用。
工作原理
按照笔者在实践中的理解,结合claude的解释,其工作原理和过程大致如下:
- 要使用expect进行命令行程序的自动化交互,通常需要配合expect并按照规范的语法,编写一个expect脚本文件;
- 在这个脚本文件中,可以使用spawn指令来启动命令行程序,它会为其启动一个子进程来执行这个命令,并监视其执行的过程和输出
- 子进程运行过程中,如果程序中断需要交互,通常子进程会提示特定的文本,可以使用expect指令来检查这些提示
- expect会查询预定义的文本匹配模式,如果匹配,则会执行预定义模式对应的动作,如发送特定的命令或者输入信息
- 在脚本文件中,可以循环这一过程,直到所有命令行的操作和执行完成。
这个过程可能会用到几个关键的指令,简单解释一下:
- set: 可以设置参数和变量
- spawn: 创建命令执行的环境,并准备处理命令交互
- expect: 声明希望匹配的提示信息
- send: 发送命令或者文本
脚本文件
前面已经提到,expect的工作原理本质上是一个自动化匹配和交互的过程,其核心就是一段相关的控制执行代码,它通常就是一个脚本语言文件,类似shell的脚本文件。这里顺便提一下,expect的脚本文件,使用的是TCL(Total Command Language)语言,在安装的过程中,也可以看到相关的依赖信息。此处我们使用的方式非常简单,就不展开讨论了。
基于以上的工作原理和前面提出的业务需求,笔者编写了一个脚本文件c.exp,内容如下:
cp.exp
#!/usr/bin/expect -f
set time_out 20
set remotepath yanjh@192.168.10.39:/xdata2/incoming/
set password [lindex $argv 0]
set localfiles ~/4copy/*
set backpath ~/4backup/
spawn bash -c "scp $localfiles $remotepath"
expect "?assword:*"
send "$password\r"
expect eof
#set localfiles [glob ~/4copy/*]
#foreach file $localfiles {
# spawn scp $file $user@$host:$remotepath
# expect "?assword:*"
# send "$password\r"
# send "\r"
#}
#expect eof
spawn bash -c "mv $localfiles $backpath"
expect eof
调用方式:
chmod a+x cp.exp
./cp.exp topsecret
简单说明如下:
- 这里基于安全考虑,将密码作为作为参数来执行脚本,注意其获取参数的指令和方式
- 其他配置信息包括超时、目标主机、目标文件夹、连接用户、本地文件夹等信息,并方便配置
- 这个命令的核心就是scp命令
- 本来应该直接执行scp和命令,但由于文件遍历的限制,需要将其封装成为整体作为字符串
- 脚本的扩展名为exp,这不重要,关键是它需要使用expect(指定命令位置)执行,在第一行进行了声明
- 脚本文件需要加执行权限( chmod a+x cp.exp)
- 如果使用遍历的方式,可能需要glob指令进行封装,并循环执行(注释的部分)
- 显然批量执行,只需要交互一次,效率应该更高一点
- 复制文件夹内容为空时,脚本会出错,但不影响正常功能
这里需要注意,笔者是将复制的源写在脚本中的。如果使用参数如 "~/4copy/*", 会有很多问题。就是只能处理第一个文件,这可能是由于shell解析通配符的限制造成的,笔者尚未找到完美的方案,期望读者中有人可以解决并予以指教。
执行的结果还是很理想的(如图),完全实现了设计目标,后续的改进可能主要是容错方面的处理了。
比较分析
经过比较copyid和expect的技术方案,我们应该可以得到以下一些结论:
- 都可以达到无需密码输入交互,实现可程序化的效果
- 但是两者的原理截然不同,copyid基于公钥认证,而expect则是模拟交互
- expect方案的灵活性要高一点,通过简单的修改配置信息,就可以适应不同的情况
- expect方案的主要不便之处是需要先安装,还要编写相关的脚本,有一定的学习使用成本
- expect安全性可能更高,因为它可以不保存任何机密信息,而是在执行时临时提供
- copyid的安全性完全取决于账号安全(账号的私钥文件),expect使用密码
- expect可能有更广泛的应用场景,可以用于任何命令行交互自动化的场景,ssh只是其中的一个用例而已
- expect基于文本匹配,需要开发人员非常熟悉命令程序的执行过程,并能够根据程序执行变化做出调整
小结
本文分析和讨论一个命令行程序的自动化交互工具-expect,包括它的基本工作原理和流程,示例程序和应用场景。