bash
#!/bin/bash
# 定义服务器列表(直接在脚本中维护)
# 将 SERVER_LIST 分为多个分类(可在原定义后添加)
# 格式为 "分组名|服务器名|用户名|IP|密码|端口"
GROUPED_SERVER_LIST=(
"测试环境|测试虚拟机single|single|192.168.1.10|123456|22"
"生产环境|服务器ai-01|test|192.168.1.80|123456|22"
)
# 更新的显示菜单函数 - 支持分组
show_menu() {
clear
echo -e "\033[34m$(printf '%.0s═' {1..70})\033[0m"
echo -e "\033[34m║$(printf '%29s' "服务器登录系统")$(printf '%37s' "")║\033[0m"
echo -e "\033[34m$(printf '%.0s═' {1..70})\033[0m"
echo -e "\033[32m$(printf "%2s %-${#servername_max_length}s %-${#username_max_length}s@%-15s %-${#group_max_length}s %s\033[0m" "编号" "服务器名称" "登录信息" "分组" "状态")"
echo -e "\033[34m$(printf '%.0s─' {1..70})\033[0m"
# 计算各列最大长度
local group_max_length=0
local servername_max_length=0
local username_max_length=0
for server_entry in "${GROUPED_SERVER_LIST[@]}"; do
IFS='|' read -r -a server_info <<< "$server_entry"
local group_name="${server_info[0]}"
local server_name="${server_info[1]}"
local username="${server_info[2]}"
if [[ ${#group_name} -gt $group_max_length ]]; then
group_max_length=${#group_name}
fi
if [[ ${#server_name} -gt $servername_max_length ]]; then
servername_max_length=${#server_name}
fi
if [[ ${#username} -gt $username_max_length ]]; then
username_max_length=${#username}
fi
done
# 设置最小宽度
group_max_length=$((group_max_length > 10 ? group_max_length : 10))
servername_max_length=$((servername_max_length > 18 ? servername_max_length : 18))
username_max_length=$((username_max_length > 8 ? username_max_length : 8))
# 记录上一个分组以显示分组标题
local last_group=""
for i in "${!GROUPED_SERVER_LIST[@]}"; do
IFS='|' read -r -a server_info <<< "${GROUPED_SERVER_LIST[i]}"
local group_name="${server_info[0]}"
local server_name="${server_info[1]}"
local username="${server_info[2]}"
local host="${server_info[3]}"
local password="${server_info[4]}"
local port="${server_info[5]}" # 添加端口信息
# 如果分组变化,显示分组标题
if [[ "$group_name" != "$last_group" ]]; then
echo -e "\033[35m$(printf '├─ %s ─────────────────────────────────────────────────────' "$group_name")\033[0m"
last_group="$group_name"
fi
local password_status=""
if [[ -z "${password}" ]]; then
password_status="\033[31m(需输入密码)\033[0m"
else
password_status="\033[32m(密码已配置)\033[0m"
fi
printf "\033[33m%2d\033[0m %-${servername_max_length}s \033[36m%-${username_max_length}s@%-15s\033[0m %-${group_max_length}s %s\n" \
$((i+1)) "$server_name" "$username" "$host" "$group_name" "$password_status"
done
echo -e "\033[34m$(printf '%.0s─' {1..70})\033[0m"
echo -e "输入 \033[31mq\033[0m 或 \033[31mquit\033[0m 退出系统"
echo -e "输入 \033[33ml\033[0m 查看连接日志"
echo -e "输入 \033[33ma\033[0m 添加新服务器"
echo -e "输入 \033[33md\033[0m 删除服务器"
echo -e "\033[34m$(printf '%.0s═' {1..70})\033[0m"
}
# 安全读取密码函数(优化版)
read_password_secure() {
local prompt="$1"
local password=""
local char=""
echo -n -e "$prompt"
# 保存当前终端设置并禁用回显
local stty_settings
stty_settings=$(stty -g 2>/dev/null)
stty -echo -icanic min 1 time 0 2>/dev/null
# 逐字符读取密码
while IFS= read -r -n1 -s char; do
# 检测回车键(空字符) - 结束输入
if [[ -z "$char" ]]; then
break
fi
# 处理退格键 (127) 或删除键 (8)
if [[ "$char" == $'\x7f' || "$char" == $'\x08' ]]; then
if [[ -n "$password" ]]; then
password="${password%?}"
printf '\b \b'
fi
# 处理Ctrl+C (中断)
elif [[ "$char" == $'\x03' ]]; then
echo -e "\n\033[33m输入已取消\033[0m"
password=""
break
# 处理普通字符
else
password+="$char"
printf '*'
fi
done
# 恢复终端设置
if [[ -n "$stty_settings" ]]; then
stty "$stty_settings" 2>/dev/null
fi
echo
echo "$password"
}
# 密码验证函数
validate_password_input() {
local hostname="$1"
local username="$2"
local port="$3"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
echo -e "\n\033[36m════════ 密码认证 ($attempt/$max_attempts) ════════\033[0m"
# 第一次输入密码
local password1=$(read_password_secure "请输入SSH密码: ")
# 检查是否取消输入
if [[ -z "$password1" ]]; then
echo -e "\033[33m密码输入已取消\033[0m"
return 1
fi
# 密码强度基础检查
if [[ ${#password1} -lt 4 ]]; then
echo -e "\033[31m密码过短,请至少输入4个字符\033[0m"
((attempt++))
continue
fi
# 首次尝试需要确认密码
if [[ $attempt -eq 1 ]]; then
echo
local password2=$(read_password_secure "请再次输入密码确认: ")
if [[ "$password1" != "$password2" ]]; then
echo -e "\033[31m错误:两次输入的密码不匹配\033[0m"
((attempt++))
continue
fi
fi
# 测试密码有效性
if test_password "$hostname" "$username" "$password1" "$port"; then
echo -e "\033[32m✓ 密码验证成功\033[0m"
echo "$password1"
return 0
else
((attempt++))
if [[ $attempt -le $max_attempts ]]; then
echo -e "\033[33m密码验证失败,请重试...\033[0m"
echo -e "\033[90m提示:检查大小写和特殊字符\033[0m"
else
echo -e "\033[31m错误:超过最大尝试次数\033[0m"
fi
fi
done
return 1
}
# 测试密码有效性
test_password() {
local host="$1"
local user="$2"
local pass="$3"
local port="${4:-22}"
echo -e "\033[36m测试密码有效性...\033[0m"
# 使用短超时快速测试密码
timeout 5 expect -c "
set timeout 5
log_user 0
spawn ssh -p $port -o ConnectTimeout=3 -o StrictHostKeyChecking=no $user@$host
expect {
\"*password:\" {
send \"$pass\\r\"
expect {
\"*Permission denied*\" { exit 1 }
\"*@*\" { send \"exit\\r\"; expect eof; exit 0 }
timeout { exit 2 }
}
}
\"*?assword:\" {
send \"$pass\\r\"
expect {
\"*Permission denied*\" { exit 1 }
\"*@*\" { send \"exit\\r\"; expect eof; exit 0 }
timeout { exit 2 }
}
}
\"*@*\" {
send \"exit\\r\"
expect eof
exit 0
}
timeout { exit 2 }
}
" >/dev/null 2>&1
local test_result=$?
case $test_result in
0) return 0 ;;
1) return 1 ;;
*) return 2 ;;
esac
}
# 日志函数
log_message() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $1" >> /tmp/ssh_login.log
}
# 测试SSH连接函数
test_ssh_connection() {
local host="$1"
local port="$2"
echo -e "\n\033[36m测试连接 $host:$port ...\033[0m"
if command -v nc &> /dev/null; then
if nc -z -w 3 "$host" "$port" &> /dev/null; then
echo -e "\033[32m✓ 端口 $port 开放\033[0m"
return 0
else
echo -e "\033[31m✗ 无法连接到 $host:$port\033[0m"
return 1
fi
else
if timeout 3 bash -c "cat < /dev/null > /dev/tcp/$host/$port" 2>/dev/null; then
echo -e "\033[32m✓ 端口 $port 开放\033[0m"
return 0
else
echo -e "\033[31m✗ 无法连接到 $host:$port\033[0m"
return 1
fi
fi
}
# 显示连接日志
show_logs() {
echo -e "\n\033[34m================ 最近连接日志 =================\033[0m"
if [[ -f "/tmp/ssh_login.log" ]]; then
if [[ -s "/tmp/ssh_login.log" ]]; then
tail -20 /tmp/ssh_login.log
else
echo "暂无连接日志记录"
fi
else
echo "日志文件不存在"
fi
echo -e "\033[34m================================================\033[0m"
read -n 1 -s -r -p "按任意键返回主菜单..."
}
# 确认退出函数
confirm_exit() {
echo -e -n "\n\033[33m确定要退出系统吗?(y/N): \033[0m"
read -n 1 confirm
echo
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
echo -e "\033[32m已退出登录,再见!\033[0m"
exit 0
else
echo -e "\033[36m取消退出,返回主菜单...\033[0m"
sleep 1
fi
}
# 创建安全的expect登录脚本
create_expect_script() {
local username="$1"
local hostip="$2"
local password="$3"
local port="$4"
local expect_script=$(mktemp)
cat > "$expect_script" << EOF
#!/usr/bin/expect -f
set timeout 15
log_user 1
# 启动SSH连接
spawn ssh -p $port -o ConnectTimeout=10 -o StrictHostKeyChecking=no $username@$hostip
expect {
"yes/no" {
send "yes\r"
exp_continue
}
"password:" {
send "$password\r"
}
"?assword:" {
send "$password\r"
}
"Permission denied" {
send_user "认证失败:密码错误或权限不足\n"
exit 1
}
"Could not resolve hostname" {
send_user "错误:无法解析主机名\n"
exit 1
}
"Connection refused" {
send_user "错误:连接被拒绝(可能SSH服务未运行)\n"
exit 1
}
"Connection timed out" {
send_user "错误:连接超时\n"
exit 1
}
"Network is unreachable" {
send_user "错误:网络不可达\n"
exit 1
}
timeout {
send_user "错误:连接超时(15秒)\n"
exit 1
}
eof {
send_user "SSH连接已关闭\n"
exit 0
}
}
# 检查登录是否成功
expect {
"*\$ " {
send_user "\n成功登录到服务器(普通用户权限),按下回车开始使用。\n"
}
"*# " {
send_user "\n成功登录到服务器(root管理员权限),按下回车开始使用。\n"
}
timeout {
send_user "\n已成功连接到服务器\n"
}
}
# 交互模式
interact
# 处理退出
expect eof {
send_user "SSH会话已结束\n"
}
EOF
echo "$expect_script"
}
# 主循环
while true; do
show_menu
read -p "请输入要登录的服务器编号: " choice
# 检查退出条件
if [[ "$choice" == "q" ]] || [[ "$choice" == "quit" ]]; then
confirm_exit
continue
fi
# 显示日志
if [[ "$choice" == "l" ]] || [[ "$choice" == "L" ]]; then
show_logs
continue
fi
# 验证输入有效性
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
echo -e "\033[31m错误:请输入有效的数字编号!\033[0m"
read -n 1 -s -r -p "按任意键继续..."
continue
fi
if [[ $choice -lt 1 ]] || [[ $choice -gt ${#GROUPED_SERVER_LIST[@]} ]]; then
echo -e "\033[31m错误:编号必须在 1-${#GROUPED_SERVER_LIST[@]} 之间!\033[0m"
read -n 1 -s -r -p "按任意键继续..."
continue
fi
# 解析服务器信息
index=$((choice-1))
IFS='|' read -r -a server_info <<< "${GROUPED_SERVER_LIST[index]}"
groupname="${server_info[0]}" # 添加分组名称
servername="${server_info[1]}" # 服务器名称现在在位置1
username="${server_info[2]}" # 用户名在位置2
hostip="${server_info[3]}" # IP在位置3
password="${server_info[4]}" # 密码在位置4
port="${server_info[5]:-22}" # 端口在位置5,默认值为22
# 日志记录
log_message "用户选择登录服务器: $servername ($username@$hostip:$port)"
# 先测试连接
if ! test_ssh_connection "$hostip" "$port"; then
echo -e "\n\033[31m连接测试失败,请检查以下可能原因:\033[0m"
echo -e "1. 服务器地址是否正确: \033[36m$hostip\033[0m"
echo -e "2. SSH端口是否开放: \033[36m$port\033[0m"
echo -e "3. 网络是否畅通"
echo -e "4. 防火墙设置是否正确"
log_message "连接测试失败: $hostip:$port"
read -n 1 -s -r -p "按任意键返回主菜单..."
continue
fi
# 密码处理逻辑
if [[ -z "$password" ]]; then
echo -e "\n\033[36m════════════ 登录认证 ════════════\033[0m"
echo -e "服务器: \033[1m$servername\033[0m"
echo -e "连接: \033[36m$username@$hostip:$port\033[0m"
password=$(validate_password_input "$hostip" "$username" "$port")
if [[ $? -ne 0 ]]; then
read -n 1 -s -r -p "按任意键返回主菜单..."
continue
fi
else
echo -e "\n\033[32m✓ 使用预设密码连接\033[0m"
# 即使有预设密码也验证一下
if ! test_password "$hostip" "$username" "$password" "$port"; then
echo -e "\033[33m预设密码可能已失效,请手动输入新密码:\033[0m"
password=$(validate_password_input "$hostip" "$username" "$port")
if [[ $? -ne 0 ]]; then
read -n 1 -s -r -p "按任意键返回主菜单..."
continue
fi
fi
fi
# 显示登录信息
echo -e "\n\033[32m════════════ 开始登录 ════════════\033[0m"
echo -e "服务器名称: \033[36m$servername\033[0m"
echo -e "服务器地址: \033[36m$hostip:$port\033[0m"
echo -e "用户账号: \033[36m$username\033[0m"
echo -e "\033[33m正在建立SSH连接,请稍候...\033[0m"
echo -e "\033[90m提示:按 Ctrl+C 可终止连接\033[0m\n"
# 记录登录尝试
log_message "开始SSH登录尝试: $username@$hostip:$port"
# 创建并执行expect脚本
expect_script=$(create_expect_script "$username" "$hostip" "$password" "$port")
# 执行expect脚本
expect -f "$expect_script"
local expect_status=$?
# 清理临时文件
rm -f "$expect_script"
# 清理密码变量
unset password
echo -e "\n\033[34m════════════ 连接总结 ════════════\033[0m"
# 根据退出状态显示不同消息
case $expect_status in
0)
echo -e "\033[32m✓ SSH连接正常结束\033[0m"
log_message "SSH连接正常结束: $username@$hostip"
;;
1)
echo -e "\033[31m✗ SSH连接失败\033[0m"
echo -e "可能的原因:"
echo -e " • 密码错误"
echo -e " • 用户名不正确"
echo -e " • 服务器拒绝连接"
log_message "SSH连接失败: $username@$hostip (错误码: $expect_status)"
;;
124)
echo -e "\033[33m⚠ SSH连接超时\033[0m"
log_message "SSH连接超时: $username@$hostip"
;;
*)
echo -e "\033[33m⚠ SSH连接异常结束 (代码: $expect_status)\033[0m"
log_message "SSH连接异常: $username@$hostip (错误码: $expect_status)"
;;
esac
# 连接结束后返回菜单
echo -e "\n\033[33m返回主菜单...\033[0m"
echo -e "\033[34m══════════════════════════════════\033[0m"
read -n 1 -s -r -p "按任意键继续..."
done