Files
acme_renew/create_new_domain_cert.sh
jeremygan2021 c26c04cf44 create
2026-03-22 23:40:37 +08:00

834 lines
27 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"