834 lines
27 KiB
Bash
Executable File
834 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# 脚本名称: create_new_domain_cert.sh
|
||
# 功能: 为新域名自动创建 Let's Encrypt SSL 证书
|
||
# 作者: Assistant
|
||
# 日期: 2026-03-22
|
||
# 用法:
|
||
# sudo ./create_new_domain_cert.sh
|
||
#
|
||
# 交互式输入:
|
||
# - 域名 (例如: example.com)
|
||
# - 后端服务端口 (例如: 3000)
|
||
# - 证书存放目录 (例如: /etc/ssl/certs/example.com)
|
||
#
|
||
# 前提条件:
|
||
# - 需要 root 权限运行
|
||
# - 需要安装 nginx
|
||
# - 需要开放 80 和 443 端口
|
||
#
|
||
# sudo chmod +x create_new_domain_cert.sh
|
||
|
||
# ================= 配置区域 =================
|
||
ACME_SH="/root/.acme.sh/acme.sh"
|
||
WEBROOT="/var/www/letsencrypt"
|
||
NGINX_CONF_DIR="/etc/nginx/conf.d"
|
||
NGINX_AVAILABLE="/etc/nginx/sites-available"
|
||
NGINX_ENABLED="/etc/nginx/sites-enabled"
|
||
EMAIL="admin@$(hostname -f)"
|
||
# ===========================================
|
||
|
||
# 彩色输出(方便阅读)
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# 日志函数 - 中文输出
|
||
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_warn() { echo -e "${YELLOW}[警告]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_error() { echo -e "${RED}[错误]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_step() { echo -e "${BLUE}[步骤]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_success() { echo -e "${CYAN}[完成]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
|
||
# 全局变量
|
||
DOMAIN=""
|
||
BACKEND_PORT=""
|
||
CERT_DIR=""
|
||
LOG_FILE=""
|
||
HTTP_CONF_FILE=""
|
||
HTTPS_CONF_FILE=""
|
||
|
||
# 检查是否 root 权限
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
log_error "请使用 root 权限运行此脚本"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 交互式收集参数
|
||
input_parameters() {
|
||
echo ""
|
||
echo -e "${BLUE}========================================${NC}"
|
||
echo -e "${BLUE} 🆕 新域名 SSL 证书创建向导 ${NC}"
|
||
echo -e "${BLUE}========================================${NC}"
|
||
echo ""
|
||
|
||
# 收集域名
|
||
while true; do
|
||
read -p "$(echo -e "${CYAN}[输入]${NC} 请输入域名(例如:example.com): ")" DOMAIN
|
||
if [[ -z "$DOMAIN" ]]; then
|
||
log_error "域名不能为空"
|
||
continue
|
||
fi
|
||
# 简单的域名格式验证
|
||
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$ ]]; then
|
||
log_error "域名格式不正确,请重新输入"
|
||
continue
|
||
fi
|
||
break
|
||
done
|
||
|
||
# 收集后端端口
|
||
while true; do
|
||
read -p "$(echo -e "${CYAN}[输入]${NC} 请输入后端服务端口(例如:3000): ")" BACKEND_PORT
|
||
if [[ -z "$BACKEND_PORT" ]]; then
|
||
log_error "端口不能为空"
|
||
continue
|
||
fi
|
||
# 端口必须是数字
|
||
if [[ ! "$BACKEND_PORT" =~ ^[0-9]+$ ]] || [[ $BACKEND_PORT -lt 1 ]] || [[ $BACKEND_PORT -gt 65535 ]]; then
|
||
log_error "端口号必须在 1-65535 之间"
|
||
continue
|
||
fi
|
||
break
|
||
done
|
||
|
||
# 收集证书目录
|
||
read -p "$(echo -e "${CYAN}[输入]${NC} 请输入证书存放目录(默认:/etc/ssl/certs/${DOMAIN}): ")" INPUT_CERT_DIR
|
||
if [[ -z "$INPUT_CERT_DIR" ]]; then
|
||
CERT_DIR="/etc/ssl/certs/${DOMAIN}"
|
||
else
|
||
CERT_DIR="$INPUT_CERT_DIR"
|
||
fi
|
||
|
||
# 设置日志文件
|
||
LOG_FILE="/var/log/provision-${DOMAIN}.log"
|
||
|
||
# 设置配置文件路径
|
||
HTTP_CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.http.conf"
|
||
HTTPS_CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.https.conf"
|
||
|
||
echo ""
|
||
log_info "📋 配置汇总:"
|
||
echo " 域名: $DOMAIN"
|
||
echo " 端口: $BACKEND_PORT"
|
||
echo " 证书目录: $CERT_DIR"
|
||
echo " 日志文件: $LOG_FILE"
|
||
echo ""
|
||
}
|
||
|
||
# 检查并安装 acme.sh
|
||
install_acme_sh() {
|
||
log_step "检查 acme.sh 安装状态..."
|
||
|
||
if [[ -f "$ACME_SH" ]]; then
|
||
log_success "acme.sh 已安装: $ACME_SH"
|
||
|
||
# 检查是否需要升级
|
||
local version=$("$ACME_SH" --version 2>/dev/null | head -n1)
|
||
log_info "当前版本: $version"
|
||
|
||
echo ""
|
||
read -p "$(echo -e "${YELLOW}[确认]${NC} 是否升级 acme.sh 到最新版本? [y/N]: ")" upgrade
|
||
if [[ "$upgrade" =~ ^[Yy]$ ]]; then
|
||
log_info "正在升级 acme.sh..."
|
||
if "$ACME_SH" --upgrade --auto-upgrade; then
|
||
log_success "acme.sh 升级成功"
|
||
else
|
||
log_warn "升级失败,继续使用当前版本"
|
||
fi
|
||
fi
|
||
else
|
||
log_warn "未检测到 acme.sh,开始自动安装..."
|
||
|
||
# 自动安装 acme.sh
|
||
log_info "执行安装命令: curl https://get.acme.sh | sh -s email=$EMAIL"
|
||
if curl https://get.acme.sh | sh -s email=$EMAIL; then
|
||
log_success "acme.sh 安装成功"
|
||
|
||
# 验证安装
|
||
if [[ -f "$ACME_SH" ]]; then
|
||
log_info "验证安装: $ACME_SH"
|
||
else
|
||
log_error "acme.sh 安装验证失败"
|
||
exit 1
|
||
fi
|
||
else
|
||
log_error "acme.sh 安装失败,请检查网络连接"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# 设置默认 CA
|
||
log_info "设置默认 CA 为 Let's Encrypt..."
|
||
"$ACME_SH" --set-default-ca --server letsencrypt >/dev/null 2>&1 || true
|
||
}
|
||
|
||
# 创建并授权 webroot 目录
|
||
setup_webroot() {
|
||
log_step "创建并授权 webroot 目录..."
|
||
|
||
if [[ ! -d "$WEBROOT" ]]; then
|
||
log_info "创建 webroot 目录: $WEBROOT"
|
||
mkdir -p "$WEBROOT"
|
||
else
|
||
log_success "webroot 目录已存在: $WEBROOT"
|
||
fi
|
||
|
||
# 设置权限
|
||
chown -R www-data:www-data "$WEBROOT"
|
||
chmod -R 755 "$WEBROOT"
|
||
|
||
log_success "webroot 目录权限设置完成"
|
||
log_info "所有者: www-data:www-data"
|
||
log_info "权限: 755"
|
||
}
|
||
|
||
# 备份旧配置(幂等性支持)
|
||
backup_old_configs() {
|
||
log_step "检查并备份旧配置..."
|
||
|
||
local backup_count=0
|
||
local timestamp=$(date +%s)
|
||
|
||
# 备份 HTTP 配置
|
||
if [[ -f "$HTTP_CONF_FILE" ]]; then
|
||
local backup_file="${HTTP_CONF_FILE}.bak-${timestamp}"
|
||
mv "$HTTP_CONF_FILE" "$backup_file"
|
||
log_info "已备份 HTTP 配置: $backup_file"
|
||
backup_count=$((backup_count + 1))
|
||
fi
|
||
|
||
# 备份 HTTPS 配置
|
||
if [[ -f "$HTTPS_CONF_FILE" ]]; then
|
||
local backup_file="${HTTPS_CONF_FILE}.bak-${timestamp}"
|
||
mv "$HTTPS_CONF_FILE" "$backup_file"
|
||
log_info "已备份 HTTPS 配置: $backup_file"
|
||
backup_count=$((backup_count + 1))
|
||
fi
|
||
|
||
# 备份证书目录
|
||
if [[ -d "$CERT_DIR" ]] && [[ -n "$(ls -A $CERT_DIR 2>/dev/null)" ]]; then
|
||
local backup_dir="${CERT_DIR}.bak-${timestamp}"
|
||
cp -r "$CERT_DIR" "$backup_dir"
|
||
log_info "已备份证书目录: $backup_dir"
|
||
backup_count=$((backup_count + 1))
|
||
fi
|
||
|
||
if [[ $backup_count -eq 0 ]]; then
|
||
log_info "未发现旧配置,无需备份"
|
||
else
|
||
log_success "共备份 $backup_count 个旧配置"
|
||
fi
|
||
}
|
||
|
||
# 检查证书是否已存在且未过期
|
||
check_existing_certificate() {
|
||
log_step "检查现有证书..."
|
||
|
||
# 使用 acme.sh --list 查看已颁发的证书
|
||
local cert_list=$("$ACME_SH" --list 2>/dev/null)
|
||
|
||
if echo "$cert_list" | grep -q "$DOMAIN"; then
|
||
log_warn "发现已存在的证书: $DOMAIN"
|
||
|
||
# 检查证书是否即将过期
|
||
local cert_file="$("$ACME_SH" --list | grep "^$DOMAIN" | awk '{print $5}')"
|
||
if [[ -n "$cert_file" ]] && [[ -f "$cert_file" ]]; then
|
||
local expire_date=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
|
||
local expire_timestamp=$(date -d "$expire_date" +%s 2>/dev/null)
|
||
local now_timestamp=$(date +%s)
|
||
local days_left=$(( (expire_timestamp - now_timestamp) / 86400 ))
|
||
|
||
log_info "证书过期时间: $expire_date"
|
||
log_info "距离过期还有: ${days_left} 天"
|
||
|
||
if [[ $days_left -gt 30 ]]; then
|
||
echo ""
|
||
echo -e "${YELLOW}现有证书仍在有效期内(>30天),请选择操作:${NC}"
|
||
echo "1) 跳过证书申请,直接安装现有证书"
|
||
echo "2) 强制重新申请证书 (Force Renew)"
|
||
echo "3) 退出脚本"
|
||
read -p "请输入选项 [1-3]: " choice
|
||
|
||
case "$choice" in
|
||
1)
|
||
log_info "用户选择: 跳过证书申请,使用现有证书"
|
||
return 0
|
||
;;
|
||
2)
|
||
log_info "用户选择: 强制重新申请证书"
|
||
return 1
|
||
;;
|
||
*)
|
||
log_info "用户选择退出"
|
||
exit 0
|
||
;;
|
||
esac
|
||
else
|
||
log_warn "证书即将过期(<30天),将重新申请"
|
||
return 1
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
log_info "未找到现有证书,将申请新证书"
|
||
return 1
|
||
}
|
||
|
||
# 生成临时 HTTP 配置用于 ACME 验证
|
||
generate_http_config() {
|
||
log_step "生成临时 HTTP 配置..."
|
||
|
||
cat > "$HTTP_CONF_FILE" <<EOF
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name ${DOMAIN};
|
||
|
||
# ACME 挑战目录
|
||
location /.well-known/acme-challenge/ {
|
||
root ${WEBROOT};
|
||
allow all;
|
||
}
|
||
|
||
# 其他请求代理到后端服务作为兜底
|
||
location / {
|
||
proxy_pass http://localhost:${BACKEND_PORT};
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
log_success "HTTP 配置已生成: $HTTP_CONF_FILE"
|
||
}
|
||
|
||
# 验证并重载 nginx
|
||
reload_nginx() {
|
||
log_step "验证 nginx 配置..."
|
||
|
||
if ! nginx -t; then
|
||
log_error "nginx 配置验证失败,请检查配置文件"
|
||
cat "$HTTP_CONF_FILE"
|
||
exit 1
|
||
fi
|
||
|
||
log_success "nginx 配置验证通过"
|
||
|
||
log_step "重载 nginx 服务..."
|
||
|
||
if systemctl reload nginx >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功"
|
||
elif service nginx reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (service 命令)"
|
||
elif nginx -s reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (nginx -s)"
|
||
else
|
||
log_error "nginx 重载失败,请手动执行: systemctl reload nginx"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 检测域名 80 端口可达性
|
||
check_domain_reachability() {
|
||
log_step "检测域名 80 端口可达性..."
|
||
|
||
local test_url="http://${DOMAIN}/.well-known/acme-challenge/test"
|
||
log_info "测试 URL: $test_url"
|
||
|
||
local response
|
||
response=$(curl -I -s --connect-timeout 10 "$test_url" 2>&1)
|
||
local http_code=$(echo "$response" | grep -i "HTTP/" | head -n1 | awk '{print $2}')
|
||
local location=$(echo "$response" | grep -i "^Location:" | head -n1 | awk '{print $2}' | tr -d '\r')
|
||
|
||
log_info "HTTP 响应码: ${http_code:-无响应}"
|
||
|
||
# 智能判断各种情况
|
||
case "$http_code" in
|
||
200)
|
||
log_success "✅ 域名 80 端口可达性检测通过 (HTTP 200)"
|
||
log_info "ACME 挑战路径正常响应"
|
||
return 0
|
||
;;
|
||
404)
|
||
log_success "✅ 域名 80 端口可达性检测通过 (HTTP 404)"
|
||
log_warn "ACME 挑战路径返回 404,但 nginx 已响应"
|
||
log_info "nginx 会自动创建挑战文件,检测通过"
|
||
return 0
|
||
;;
|
||
301|302)
|
||
log_warn "检测到 HTTP 跳转 (HTTP $http_code)"
|
||
if [[ -n "$location" ]]; then
|
||
log_info "跳转目标: $location"
|
||
|
||
# 检查是否是跳转到 HTTPS
|
||
if [[ "$location" =~ ^https:// ]]; then
|
||
log_warn "⚠️ HTTPS 跳转已配置生效!"
|
||
log_info "这说明之前已经成功配置过 HTTPS"
|
||
|
||
# 判断是否需要继续(取决于场景)
|
||
echo ""
|
||
echo -e "${YELLOW}检测到域名 $DOMAIN 已配置 HTTPS 跳转${NC}"
|
||
echo "这可能是之前运行的配置残留"
|
||
echo ""
|
||
|
||
# 智能决策:检查是否真的需要跳转
|
||
# 如果 Location 是空的或指向错误位置,可能是配置问题
|
||
if [[ "$location" == "https://" ]] || [[ "$location" == "https://$DOMAIN" ]] || [[ "$location" =~ ^https://.*$ ]]; then
|
||
log_info "HTTPS 跳转配置正常,域名可达"
|
||
log_info "可以继续证书申请流程"
|
||
return 0
|
||
else
|
||
log_error "跳转目标异常: $location"
|
||
return 1
|
||
fi
|
||
else
|
||
log_warn "跳转到非 HTTPS 地址: $location"
|
||
return 1
|
||
fi
|
||
else
|
||
log_warn "收到 HTTP $http_code 但无 Location 头"
|
||
return 1
|
||
fi
|
||
;;
|
||
000)
|
||
log_error "❌ 连接失败 (HTTP $http_code)"
|
||
log_error "域名可能未解析或防火墙阻止"
|
||
return 1
|
||
;;
|
||
*)
|
||
log_error "❌ 意外的 HTTP 响应码: $http_code"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# 申请 Let's Encrypt 证书
|
||
issue_certificate() {
|
||
log_step "申请 Let's Encrypt 证书..."
|
||
|
||
log_info "执行命令: $ACME_SH --issue -d $DOMAIN --webroot $WEBROOT"
|
||
echo ""
|
||
|
||
# 捕获输出用于分析
|
||
local tmp_out=$(mktemp)
|
||
local tmp_err=$(mktemp)
|
||
|
||
# 执行证书申请
|
||
"$ACME_SH" --issue -d "$DOMAIN" --webroot "$WEBROOT" > >(tee -a "$tmp_out") 2> >(tee -a "$tmp_err" >&2)
|
||
local ret=$?
|
||
|
||
# 合并所有输出
|
||
local all_output=$(cat "$tmp_out" "$tmp_err" 2>/dev/null)
|
||
|
||
# 智能识别各种成功/跳过情况
|
||
local success=false
|
||
local skip_reason=""
|
||
|
||
# 情况1: 返回码为0且包含证书
|
||
if [[ $ret -eq 0 ]] && echo "$all_output" | grep -q "-----END CERTIFICATE-----"; then
|
||
success=true
|
||
log_success "✅ 证书申请成功!(新证书)"
|
||
fi
|
||
|
||
# 情况2: 域名无变化,跳过申请(acme.sh认为不需要重复申请)
|
||
if echo "$all_output" | grep -qE "Domains not changed|Skipping|already issued|Your cert is in"; then
|
||
success=true
|
||
skip_reason=$(echo "$all_output" | grep -E "Domains not changed|Skipping|already issued|Your cert is in" | head -n1)
|
||
|
||
# 提取证书路径
|
||
local cert_path=$(echo "$all_output" | grep "Your cert is in:" | head -n1 | sed 's/.*Your cert is in: //')
|
||
if [[ -n "$cert_path" ]]; then
|
||
log_success "✅ 证书已存在且有效"
|
||
log_info "证书位置: $cert_path"
|
||
|
||
# 验证证书文件是否存在
|
||
if [[ -f "$cert_path" ]] || [[ -f "${cert_path%.cer}.pem" ]] || [[ -f "${cert_path%.cer}.key" ]]; then
|
||
log_info "✅ 证书文件验证通过"
|
||
fi
|
||
fi
|
||
|
||
if echo "$all_output" | grep -q "Domains not changed"; then
|
||
log_info "证书信息未变化,跳过重复申请"
|
||
fi
|
||
if echo "$all_output" | grep -q "Skipping"; then
|
||
local next_renewal=$(echo "$all_output" | grep "Next renewal time" | head -n1)
|
||
log_info "$next_renewal"
|
||
fi
|
||
fi
|
||
|
||
# 情况3: 返回码非0,但证书已存在(某些版本的acme.sh会这样)
|
||
if [[ $ret -ne 0 ]] && [[ -f "/root/.acme.sh/${DOMAIN}_ecc/${DOMAIN}.cer" ]] || [[ -f "/root/.acme.sh/${DOMAIN}/${DOMAIN}.cer" ]]; then
|
||
success=true
|
||
log_warn "⚠️ acme.sh 返回非零 ($ret),但证书已存在"
|
||
log_info "继续使用现有证书"
|
||
|
||
# 显示错误信息(但不退出)
|
||
echo ""
|
||
log_warn "警告信息:"
|
||
echo "$all_output" | tail -n 5
|
||
fi
|
||
|
||
# 清理临时文件
|
||
rm -f "$tmp_out" "$tmp_err"
|
||
|
||
# 最终判断
|
||
if [[ "$success" == "true" ]]; then
|
||
return 0
|
||
else
|
||
# 真正的错误情况
|
||
log_error "❌ 证书申请失败 (返回码: $ret)"
|
||
echo ""
|
||
echo "========== 完整输出 =========="
|
||
echo "$all_output"
|
||
echo "==============================="
|
||
echo ""
|
||
log_error "请检查以上输出并确认:"
|
||
echo " 1. 域名是否正确解析到此服务器"
|
||
echo " 2. 防火墙是否开放 80 和 443 端口"
|
||
echo " 3. nginx 是否正常运行"
|
||
echo " 4. webroot 目录是否可写"
|
||
echo ""
|
||
log_error "详细日志: $LOG_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 安装证书到指定位置
|
||
install_certificate() {
|
||
log_step "安装证书到指定目录..."
|
||
|
||
# 确保目录存在
|
||
mkdir -p "$CERT_DIR"
|
||
|
||
log_info "证书安装目录: $CERT_DIR"
|
||
|
||
# 使用 acme.sh 安装证书
|
||
local install_cmd="$ACME_SH --install-cert -d $DOMAIN \
|
||
--key-file ${CERT_DIR}/key.pem \
|
||
--fullchain-file ${CERT_DIR}/cert.pem \
|
||
--reloadcmd \"systemctl force-reload nginx\""
|
||
|
||
log_info "执行安装命令..."
|
||
echo "$install_cmd" | tee -a "$LOG_FILE"
|
||
|
||
if eval "$install_cmd"; then
|
||
log_success "证书安装成功"
|
||
else
|
||
log_error "证书安装失败"
|
||
exit 1
|
||
fi
|
||
|
||
# 设置文件权限
|
||
log_step "设置证书文件权限..."
|
||
|
||
if chown www-data:www-data "${CERT_DIR}"/*.pem 2>/dev/null; then
|
||
log_success "文件所有者已设置为 www-data:www-data"
|
||
else
|
||
log_warn "chown 执行失败,请手动检查权限"
|
||
fi
|
||
|
||
if chmod 600 "${CERT_DIR}"/*.pem 2>/dev/null; then
|
||
log_success "文件权限已设置为 600"
|
||
else
|
||
log_warn "chmod 执行失败,请手动检查权限"
|
||
fi
|
||
|
||
# 验证证书文件
|
||
log_step "验证证书文件..."
|
||
if [[ -f "${CERT_DIR}/cert.pem" ]]; then
|
||
log_success "证书文件存在: ${CERT_DIR}/cert.pem"
|
||
openssl x509 -in "${CERT_DIR}/cert.pem" -noout -subject -dates 2>/dev/null | tee -a "$LOG_FILE"
|
||
else
|
||
log_error "证书文件不存在: ${CERT_DIR}/cert.pem"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -f "${CERT_DIR}/key.pem" ]]; then
|
||
log_success "密钥文件存在: ${CERT_DIR}/key.pem"
|
||
else
|
||
log_error "密钥文件不存在: ${CERT_DIR}/key.pem"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 生成最终 HTTPS 配置
|
||
generate_https_config() {
|
||
log_step "生成 HTTPS 配置..."
|
||
|
||
# 使用 cat <<'EOF' 来防止 bash 解释 $ 变量
|
||
cat > "$HTTPS_CONF_FILE" <<'EOF'
|
||
# HTTP 到 HTTPS 的强制跳转
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name DOMAIN_PLACEHOLDER;
|
||
|
||
# 正确的 301 跳转配置
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
|
||
# HTTPS 服务器块
|
||
server {
|
||
listen 443 ssl http2;
|
||
listen [::]:443 ssl http2;
|
||
server_name DOMAIN_PLACEHOLDER;
|
||
|
||
# SSL 证书配置
|
||
ssl_certificate CERT_DIR_PLACEHOLDER/cert.pem;
|
||
ssl_certificate_key CERT_DIR_PLACEHOLDER/key.pem;
|
||
|
||
# SSL 安全配置
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||
ssl_prefer_server_ciphers off;
|
||
|
||
# HSTS 配置(可选,取消注释以启用)
|
||
# add_header Strict-Transport-Security "max-age=31536000" always;
|
||
|
||
# 日志配置
|
||
access_log /var/log/nginx/DOMAIN_PLACEHOLDER.access.log;
|
||
error_log /var/log/nginx/DOMAIN_PLACEHOLDER.error.log;
|
||
|
||
# 后端服务代理
|
||
location / {
|
||
proxy_pass http://localhost:PORT_PLACEHOLDER;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
# 使用 sed 替换占位符
|
||
sed -i "s/DOMAIN_PLACEHOLDER/${DOMAIN}/g" "$HTTPS_CONF_FILE"
|
||
sed -i "s|CERT_DIR_PLACEHOLDER|${CERT_DIR}|g" "$HTTPS_CONF_FILE"
|
||
sed -i "s/PORT_PLACEHOLDER/${BACKEND_PORT}/g" "$HTTPS_CONF_FILE"
|
||
|
||
log_success "HTTPS 配置已生成: $HTTPS_CONF_FILE"
|
||
log_info "配置内容预览:"
|
||
head -n 15 "$HTTPS_CONF_FILE"
|
||
}
|
||
|
||
# 最终验证
|
||
final_verification() {
|
||
log_step "开始最终验证流程..."
|
||
|
||
# 清理所有旧的相关配置文件
|
||
log_info "清理旧配置文件..."
|
||
|
||
# 删除临时 HTTP 配置
|
||
if [[ -f "$HTTP_CONF_FILE" ]]; then
|
||
rm -f "$HTTP_CONF_FILE"
|
||
log_info "已删除临时 HTTP 配置: $HTTP_CONF_FILE"
|
||
fi
|
||
|
||
# 删除旧的符号链接
|
||
if [[ -L "${NGINX_ENABLED}/${DOMAIN}.conf" ]]; then
|
||
rm -f "${NGINX_ENABLED}/${DOMAIN}.conf"
|
||
log_info "已删除旧符号链接: ${NGINX_ENABLED}/${DOMAIN}.conf"
|
||
fi
|
||
|
||
# 删除 sites-available 中的旧配置
|
||
if [[ -f "${NGINX_AVAILABLE}/${DOMAIN}.conf" ]]; then
|
||
rm -f "${NGINX_AVAILABLE}/${DOMAIN}.conf"
|
||
log_info "已删除旧配置: ${NGINX_AVAILABLE}/${DOMAIN}.conf"
|
||
fi
|
||
|
||
# 删除 sites-enabled 中的旧符号链接
|
||
if [[ -L "${NGINX_ENABLED}/${DOMAIN}.conf" ]]; then
|
||
rm -f "${NGINX_ENABLED}/${DOMAIN}.conf"
|
||
log_info "已删除旧符号链接: ${NGINX_ENABLED}/${DOMAIN}.conf"
|
||
fi
|
||
|
||
# 等待一下确保配置已清理
|
||
sleep 1
|
||
|
||
# 验证 nginx 配置
|
||
log_step "验证 nginx 配置..."
|
||
if ! nginx -t; then
|
||
log_error "nginx 配置验证失败"
|
||
log_error "请检查 /etc/nginx/conf.d 目录下的配置文件"
|
||
exit 1
|
||
fi
|
||
log_success "nginx 配置验证通过"
|
||
|
||
# 重启 nginx 服务(确保完全重新加载配置)
|
||
log_step "重启 nginx 服务..."
|
||
if systemctl restart nginx >/dev/null 2>&1; then
|
||
log_success "nginx 重启成功"
|
||
elif systemctl reload nginx >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功"
|
||
elif service nginx restart >/dev/null 2>&1; then
|
||
log_success "nginx 重启成功 (service 命令)"
|
||
elif service nginx reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (service 命令)"
|
||
elif nginx -s reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (nginx -s)"
|
||
else
|
||
log_error "nginx 重启/重载失败,请手动执行: systemctl restart nginx"
|
||
exit 1
|
||
fi
|
||
|
||
# 测试域名访问
|
||
log_step "测试域名访问..."
|
||
echo ""
|
||
|
||
# 测试 HTTPS 访问
|
||
echo -e "${CYAN}测试 1: HTTPS 访问${NC}"
|
||
local https_response=$(curl -I -s --connect-timeout 10 "https://${DOMAIN}" 2>&1)
|
||
local https_code=$(echo "$https_response" | grep -i "HTTP/" | head -n1 | awk '{print $2}')
|
||
|
||
if [[ "$https_code" == "200" ]] || [[ "$https_code" == "301" ]] || [[ "$https_code" == "302" ]]; then
|
||
log_success "✅ HTTPS 访问成功 (HTTP $https_code)"
|
||
else
|
||
log_warn "⚠️ HTTPS 响应异常: ${https_code:-无响应}"
|
||
fi
|
||
|
||
# 测试 HTTP 跳转
|
||
echo ""
|
||
echo -e "${CYAN}测试 2: HTTP 跳转测试${NC}"
|
||
local http_response=$(curl -I -s --connect-timeout 10 "http://${DOMAIN}" 2>&1)
|
||
local http_code=$(echo "$http_response" | grep -i "HTTP/" | head -n1 | awk '{print $2}')
|
||
local location=$(echo "$http_response" | grep -i "^Location:" | head -n1 | sed 's/Location: *//i' | tr -d '\r')
|
||
|
||
if [[ "$http_code" == "301" ]] || [[ "$http_code" == "302" ]]; then
|
||
log_success "✅ HTTP 跳转正常 (HTTP $http_code)"
|
||
if [[ -n "$location" ]]; then
|
||
log_info "跳转目标: $location"
|
||
|
||
# 验证跳转目标是否正确
|
||
if [[ "$location" =~ ^https://${DOMAIN} ]] || [[ "$location" =~ ^https://${DOMAIN}/ ]]; then
|
||
log_success "✅ 跳转目标正确: $location"
|
||
else
|
||
log_warn "⚠️ 跳转目标可能不正确: $location"
|
||
log_info "期望格式: https://${DOMAIN}..."
|
||
fi
|
||
else
|
||
log_warn "⚠️ 收到 HTTP $http_code 但没有 Location 头"
|
||
log_error "这可能导致浏览器无法自动跳转"
|
||
fi
|
||
else
|
||
log_warn "⚠️ HTTP 跳转异常: ${http_code:-无响应}"
|
||
echo ""
|
||
log_error "完整响应头:"
|
||
echo "$http_response" | head -n 10
|
||
echo ""
|
||
log_error "如果显示 nginx 默认页面,可能原因:"
|
||
echo " 1. nginx 使用了旧的配置文件"
|
||
echo " 2. 80 端口有多个 server 块冲突"
|
||
echo " 3. 需要完全重启 nginx 而不是重载"
|
||
fi
|
||
|
||
# 验证 SSL 证书
|
||
echo ""
|
||
echo -e "${CYAN}测试 3: SSL 证书验证${NC}"
|
||
local ssl_verify=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null)
|
||
if [[ -n "$ssl_verify" ]]; then
|
||
log_success "✅ SSL 证书有效"
|
||
echo "$ssl_verify" | while read line; do
|
||
log_info " $line"
|
||
done
|
||
else
|
||
log_warn "⚠️ SSL 证书验证失败"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}========================================${NC}"
|
||
echo -e "${GREEN} ✅ SSL 证书创建成功! ${NC}"
|
||
echo -e "${GREEN}========================================${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}📋 配置信息:${NC}"
|
||
echo " 域名: $DOMAIN"
|
||
echo " 证书文件: ${CERT_DIR}/cert.pem"
|
||
echo " 密钥文件: ${CERT_DIR}/key.pem"
|
||
echo " 后端端口: $BACKEND_PORT"
|
||
echo " Nginx 配置: $HTTPS_CONF_FILE"
|
||
echo " 日志文件: $LOG_FILE"
|
||
echo ""
|
||
echo -e "${CYAN}🧪 验证命令:${NC}"
|
||
echo " 1. 测试 HTTPS 访问:"
|
||
echo -e " ${GREEN}curl -I https://${DOMAIN}${NC}"
|
||
echo ""
|
||
echo " 2. 查看证书详情:"
|
||
echo -e " ${GREEN}openssl x509 -in ${CERT_DIR}/cert.pem -noout -text | head -20${NC}"
|
||
echo ""
|
||
echo " 3. 在线验证 SSL 配置:"
|
||
echo -e " ${GREEN}https://www.ssllabs.com/ssltest/analyze.html?d=${DOMAIN}${NC}"
|
||
echo ""
|
||
echo -e "${CYAN}🔧 手动诊断命令(如遇问题):${NC}"
|
||
echo " # 查看生成的配置文件:"
|
||
echo -e " ${GREEN}cat $HTTPS_CONF_FILE${NC}"
|
||
echo ""
|
||
echo " # 检查 80 端口的 server 配置:"
|
||
echo -e " ${GREEN}grep -A 5 'listen 80' $HTTPS_CONF_FILE${NC}"
|
||
echo ""
|
||
echo " # 测试 nginx 配置:"
|
||
echo -e " ${GREEN}nginx -t && systemctl restart nginx${NC}"
|
||
echo ""
|
||
echo " # 手动测试 301 跳转(查看 Location 头):"
|
||
echo -e " ${GREEN}curl -I -v http://${DOMAIN} 2>&1 | grep -i location${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ 提示:${NC}"
|
||
echo " - HTTP 请求会自动 301 跳转到 HTTPS"
|
||
echo " - 证书将在 60 天后自动续期"
|
||
echo " - 如需手动续期: $ACME_SH --renew -d $DOMAIN"
|
||
echo ""
|
||
}
|
||
|
||
# 主流程
|
||
main() {
|
||
check_root
|
||
input_parameters
|
||
|
||
echo ""
|
||
log_info "开始执行证书创建流程..."
|
||
echo ""
|
||
|
||
install_acme_sh
|
||
setup_webroot
|
||
backup_old_configs
|
||
|
||
# 检查是否需要申请新证书
|
||
local skip_issue=false
|
||
if check_existing_certificate; then
|
||
skip_issue=true
|
||
else
|
||
skip_issue=false
|
||
fi
|
||
|
||
generate_http_config
|
||
reload_nginx
|
||
|
||
# 只有在需要申请新证书时才执行验证和申请流程
|
||
if [[ "$skip_issue" == "false" ]]; then
|
||
if check_domain_reachability; then
|
||
issue_certificate
|
||
else
|
||
log_error "域名可达性检测失败,无法继续"
|
||
exit 1
|
||
fi
|
||
else
|
||
log_info "跳过证书申请步骤"
|
||
fi
|
||
|
||
install_certificate
|
||
generate_https_config
|
||
final_verification
|
||
|
||
echo ""
|
||
log_success "🎉 域名 [$DOMAIN] SSL 证书创建完成!"
|
||
echo ""
|
||
}
|
||
|
||
# 执行主函数
|
||
main "$@"
|