Linux 远程无密传输实现脚本
本文记录了通过 SSH Key 实现 Linux 服务器间无密码远程传输文件的脚本编写过程。核心原理是使用 ssh-keygen 生成公私钥对,将公钥追加到目标服务器的 ~/.ssh/authorized_keys 实现免密登录。对于 scp/ssh 命令的自动化交互,使用 expect 工具编写脚本自动应答密码提示。文章给出了完整的脚本实现,实现了开发、测试、预上线等多环境间的一键文件传输和部署,将原本需要手动完成的重复性工作自动化。
序言
我在开发过程中,除了要部署后台的微服务,还要对前端的 WEB 目录进行部署。 虽然每次只需要 2 分钟左右,但却是机械的重复性劳动。我认为很 xx!
听说过俄罗斯有一位程序员,认为超过 90 秒以上的事情,就必须要做成脚本,比如煮咖啡、骗老婆说要加班等等……
所以我决定花几个小时时间,把这种事情写成脚本。
目前公司的前端 WEB 目录部署其实就是文件的上传下载,不需要启动什么进程。既然前端人员不想用 Linux 也不想用 FTP。那么……我在 Linux 下编写成脚本后,让前端人员直接在 Windows 下通过一个按钮点击,就完成 WEB 目录的一键部署。
这样,我就完全可以从这件事中脱身了。
一、场景描述
首先梳理一下预上线环境部署 WEB 目录的流程:
- 开发环境: xxx.xxx.x.213
- 测试环境: xxx.xxx.x.212
- 预上线环境: xxx.xxx.x.110
前两个环境是我来手动维护,因为之前已经写了很多脚本(自动 Git 拉取、Maven 打包、一键杀死/启动微服务等),所以维护起来很轻松。
而 110 环境是通过 Jenkins 来构建的,每次构建完成都会生成全新的虚拟机镜像(服务器、数据库、Redis 都会是新的,只有 IP 不变)。
所以 WEB 目录的部署逻辑是:在 212 上拉取测试通过的目录,传输到预上线的 WEB 目录。但是经常会有 bug 修正,每次修正完又要重新拉一遍。
看起来这个脚本应该很简单,就是把主机 A 的 WEB 目录 copy 到主机 B 嘛。
二、建立通信机制
但是涉及到两台机器通信,就会遇到密码验证的问题。总不能执行脚本之后再卡在终端让人工输入密码吧?
解决这个问题主要有以下两种方式:
- SSH 免密:建立互信关系。
- Expect 脚本:自动模拟人工输入密码。
方式一:SSH 免密登录
关于什么是 SSH、公钥、密钥原理什么的可以 Google,这里简单介绍下操作方法。
假设现在有两台 Linux (CentOS 7) 机器:
- 机器 A:
192.168.100.212 - 机器 B:
192.168.100.213
要实现从 A 用 SSH 无密码远程登录到 B:
1. 首先在 A 上生成公钥
ssh-keygen -t rsa这里一直点回车即可(不要输入密码,就是空密码)。
执行完成后,在 ~/.ssh/ 下就会生成 id_rsa 和 id_rsa.pub 两个文件。
2. 把 A 机器刚才生成的 id_rsa.pub 文件复制到 B 上
scp ~/.ssh/id_rsa.pub root@192.168.100.213:/root/然后在 B 的 root 目录下就能看到 id_rsa.pub 了(这里放在 root 下只是临时放一下)。
3. 在 B 机器中,将公钥内容追加到 authorized_keys 文件
cat /root/id_rsa.pub >> ~/.ssh/authorized_keys如果
~/下没有.ssh目录或者没有authorized_keys文件,就自己手动创建。
4. 在 B 上重启 sshd 服务
service ssh restart配置完成!现在在 A 上试试登录 B:ssh root@192.168.100.213,应该就不需要输入密码了。
踩坑注意事项:
- 要用哪个用户远程登录,就把
id_rsa.pub复制到该用户对应的路径下。如:root 用户就复制到/root/下;普通用户就复制到/home/xxx/下,不要混了。- B 机器上的文件权限非常严格:
.ssh文件夹必须是700;authorized_keys必须是600。
方式二:使用 Expect 自动输入
在我写这个脚本的时候,并没有使用上面那种 SSH 免密的方法。 原因是上面提到过,每次预上线环境(110)构建完都会是一台全新的虚拟机,不可能每次构建完都去人工配一次 SSH 信任关系。
所以这里介绍一种更优雅的方式:在脚本里自动输入密码(当屏幕交互需要输入时,由脚本捕获并自动发送输入)。 这里用到的工具就是 Expect。
1. 安装 Expect
sudo yum install expect2. 编写 scp.exp 脚本
#!/usr/bin/expect
set timeout 20
if { [llength $argv] < 2} {
puts "Usage:"
puts "$argv0 local_file remote_path"
exit 1
}
set local_file [lindex $argv 0]
set remote_path [lindex $argv 1]
# 注意:在现代安全规范中,极其不推荐在脚本中硬编码明文密码!
# 生产环境中建议结合 CI/CD 工具的安全环境变量(Secret)来传递。
set passwd your_passwd
set passwderror 0
spawn scp $local_file $remote_path
expect {
"*assword:*" {
if { $passwderror == 1 } {
puts "passwd is error"
exit 2
}
set timeout 1000
set passwderror 1
send "$passwd\r"
exp_continue
}
"*es/no)?*" {
send "yes\r"
exp_continue
}
timeout {
puts "connect is timeout"
exit 3
}
}这里我找到了一个比较通用的模板(除了能自动输入密码之外,还支持首次连接时 RSA 验证的 yes/no 自动选择)。
语法说明: 可以看到第一行并不是
#!/bin/bash,所以这段脚本是由expect解释执行的,语法也和 Shell 不一样。
[lindex $argv 0]:获取传入的第一个参数(本地要拷贝的文件)。[lindex $argv 1]:获取传入的第二个参数(远程目标地址)。spawn scp ...:派生出一个子进程来执行实际的 scp 拷贝命令。
执行示例: 例如我要把本机(213)的某个文件复制到主机 110 上:
./scp.exp /hahah/hello.py root@192.168.120.110:/xx/yy/踩坑记录:Expect 中执行 scp 目录的坑
在使用上述 Expect 脚本进行 scp 拷贝时,如果涉及到通配符 *,会有一个坑:
./scp.exp /hahah/hello.py root@ip:/xx/yy/:拷贝单个文件,没问题。./scp.exp /hahah/dir1 root@ip:/xx/yy/:拷贝整个目录,没问题。./scp.exp /hahah/dir1/* root@ip:/xx/yy/:按理说是把dir1目录下的所有文件(不包括 dir1 目录本身)拷贝过去。这在原生的 Shell scp 命令下是没问题的,但由于这里执行的是scp.exp,它会直接把*当成特殊字符处理导致报错。
解决办法:需要用反斜杠 \ 来转义星号。
./scp.exp /hahah/dir1/\* root@192.168.120.110:/xx/yy/三、整合调度脚本
两种通信方式介绍完了。因为我要通过一个前端的 HTTP 请求去调用拷贝动作,为了方便,我又写了一个 Bash 脚本去统筹调用刚才的 scp.exp。
update_pre_online.sh 内容如下:
#!/bin/bash
# 为了让前端调用时能正常返回文本信息
echo "Content-Type: text/html"
echo ""
# 源路径与目标路径配置
SRC_PATH=/home/www/update_pre_online/xxws-web-120/
TAG_PATH=/sysroot/tmp/xxws/
TAG_ADDRESS=192.168.120.110
# 1. 先把测试环境(212)的 WEB 目录拉取到当前执行脚本的机器(213)上
scp -r root@192.168.100.212:/home/www/xxws-web/* ${SRC_PATH}
# 2. 赋予访问权限
chmod -R 755 ${SRC_PATH}
echo "============================"
echo "递归修改目录访问权限(755)..."
echo "============================"
sleep 1
# 3. 调用 expect 脚本,将本地拉取好的文件推送到预上线环境(110)
# 注意这里的星号转义:${SRC_PATH}\*
/home/www/update_pre_online/scp.exp ${SRC_PATH}\* root@${TAG_ADDRESS}:${TAG_PATH}
# 4. 输出日志与结果
cat ~/update_pre_online.log
echo "--------------------------------------"
echo " 更新 212WEB目录 到 110WEB目录 成功! "
echo "--------------------------------------"
sleep 2
exit 0总结
完成后,在后台管理系统加一个小页面按钮,前端开发同学点一下就行了,效果如下:

虽然以前熟练的话,手动敲命令部署每次只需要三分钟,但我编写脚本加测试却用了好几个小时。
成果就是:现在每次部署只需要点击按钮后等待 5 秒即可。最重要的是!!!不需要我来操作了!
The End