#!/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/nginx/tls/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/nginx/tls/${DOMAIN}): ")" INPUT_CERT_DIR if [[ -z "$INPUT_CERT_DIR" ]]; then CERT_DIR="/etc/nginx/tls/${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 } # 清理所有可能冲突的 nginx 配置 cleanup_nginx_conflicts() { log_step "清理 nginx 配置冲突..." local cleaned=0 # 1. 清理所有包含此域名的配置文件 log_info "搜索包含 ${DOMAIN} 的配置文件..." local conflict_files=$(grep -rl "server_name.*${DOMAIN}" /etc/nginx/ 2>/dev/null | grep -v ".bak-" | grep -v ".bak/") if [[ -n "$conflict_files" ]]; then echo "$conflict_files" | while read file; do log_warn "发现冲突配置: $file" # 检查是否有多个 server_name 指令 local count=$(grep -c "server_name" "$file" 2>/dev/null || echo 0) if [[ $count -gt 1 ]]; then # 如果文件中有多个域名,只移除此域名的配置 log_warn "文件包含多个域名,仅移除此域名的配置" # 这比较复杂,暂时标记为需要手动处理 log_error "需要手动处理: $file" elif [[ $count -eq 1 ]]; then # 如果文件只有此域名,可以安全删除 local backup="${file}.bak-conflict-$(date +%s)" mv "$file" "$backup" log_info "已备份并移除冲突配置: $backup" cleaned=$((cleaned + 1)) fi done fi # 2. 清理 sites-enabled 中的符号链接 if [[ -L "${NGINX_ENABLED}/${DOMAIN}.conf" ]]; then rm -f "${NGINX_ENABLED}/${DOMAIN}.conf" log_info "已删除符号链接: ${NGINX_ENABLED}/${DOMAIN}.conf" cleaned=$((cleaned + 1)) fi # 3. 清理 sites-available 中的配置 if [[ -f "${NGINX_AVAILABLE}/${DOMAIN}.conf" ]]; then rm -f "${NGINX_AVAILABLE}/${DOMAIN}.conf" log_info "已删除配置: ${NGINX_AVAILABLE}/${DOMAIN}.conf" cleaned=$((cleaned + 1)) fi # 4. 清理 conf.d 中所有相关配置 local confd_files=$(find /etc/nginx/conf.d/ -name "*${DOMAIN}*.conf" -type f 2>/dev/null) echo "$confd_files" | while read file; do # 排除当前要生成的配置文件 if [[ "$file" != "$HTTPS_CONF_FILE" ]] && [[ "$file" != "$HTTP_CONF_FILE" ]]; then local backup="${file}.bak-old-$(date +%s)" mv "$file" "$backup" log_info "已备份旧配置: $backup" cleaned=$((cleaned + 1)) fi done # 5. 检查其他可能的配置位置 for dir in /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/nginx/conf.d; do if [[ -d "$dir" ]]; then local other_files=$(find "$dir" -name "*.conf" -type f 2>/dev/null) echo "$other_files" | while read file; do if grep -q "server_name.*${DOMAIN}" "$file" 2>/dev/null; then if [[ "$file" != "$HTTPS_CONF_FILE" ]] && [[ "$file" != "$HTTP_CONF_FILE" ]]; then local backup="${file}.bak-extra-$(date +%s)" mv "$file" "$backup" log_warn "发现额外配置并已备份: $backup" cleaned=$((cleaned + 1)) fi fi done fi done if [[ $cleaned -eq 0 ]]; then log_info "未发现配置冲突" else log_success "已清理 $cleaned 个冲突配置" log_warn "建议运行: systemctl restart nginx" 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" </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 } # 带重试的证书申请函数 issue_certificate_with_retry() { local max_retries=3 local retry_count=0 local wait_seconds=5 while [[ $retry_count -lt $max_retries ]]; do retry_count=$((retry_count + 1)) echo "" log_step "尝试申请证书 (第 $retry_count/$max_retries 次)..." # 调用基本的证书申请函数 if issue_certificate; then log_success "✅ 证书申请成功!" return 0 else # 申请失败,分析原因 log_warn "⚠️ 第 $retry_count 次申请失败" # 检查是否是配置问题导致的失败 if grep -q "Invalid response.*404" /var/log/provision-${DOMAIN}.log 2>/dev/null || grep -q "conflicting server name" /var/log/nginx/error.log 2>/dev/null; then log_warn "检测到配置冲突问题,尝试清理并重试..." # 再次清理配置 cleanup_nginx_conflicts # 重新生成 HTTP 配置 generate_http_config # 完全重启 nginx log_info "重启 nginx 服务..." if systemctl restart nginx >/dev/null 2>&1 || systemctl reload nginx >/dev/null 2>&1 || service nginx restart >/dev/null 2>&1; then log_success "nginx 重启成功" else log_error "nginx 重启失败,请手动检查" fi # 等待一下让 nginx 完全启动 sleep $wait_seconds fi # 如果不是最后一次尝试,等待后重试 if [[ $retry_count -lt $max_retries ]]; then echo "" log_info "等待 ${wait_seconds} 秒后重试..." sleep $wait_seconds # 增加等待时间(指数退避) wait_seconds=$((wait_seconds * 2)) fi fi done # 所有重试都失败 log_error "❌ 经过 $max_retries 次尝试,证书申请仍然失败" echo "" log_error "可能的原因:" echo " 1. 域名未正确解析到本服务器" echo " 2. 防火墙阻止了 80 端口" echo " 3. nginx 配置存在冲突" echo " 4. webroot 目录权限不足" echo "" echo "建议的排查步骤:" echo " # 1. 检查域名解析" echo " dig ${DOMAIN}" echo "" echo " # 2. 测试 80 端口" echo " curl -I http://${DOMAIN}/.well-known/acme-challenge/test" echo "" echo " # 3. 检查 nginx 配置" echo " grep -r 'server_name.*${DOMAIN}' /etc/nginx/" echo "" echo " # 4. 检查 webroot 权限" echo " ls -la ${WEBROOT}" echo " sudo chown -R www-data:www-data ${WEBROOT}" echo "" return 1 } # 安装证书到指定位置 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 # 清理可能存在的 nginx 配置冲突 cleanup_nginx_conflicts # 检查是否需要申请新证书 local skip_issue=false if check_existing_certificate; then skip_issue=true else skip_issue=false fi generate_http_config # 在生成配置后再次清理冲突 cleanup_nginx_conflicts reload_nginx # 只有在需要申请新证书时才执行验证和申请流程 if [[ "$skip_issue" == "false" ]]; then if check_domain_reachability; then issue_certificate_with_retry else log_error "域名可达性检测失败,无法继续" exit 1 fi else log_info "跳过证书申请步骤" fi install_certificate generate_https_config final_verification echo "" log_success "🎉 域名 [$DOMAIN] SSL 证书创建完成!" echo "" } # 执行主函数 main "$@"