04d5024752
- 创建 SSL 证书申请脚本 (tools/deploy-ssl-cert.py) - 创建 nginx HTTPS 配置文件 (conf/nginx-emotion-museum-ssl.conf) - 创建 nginx HTTP 修复配置 (conf/nginx-emotion-museum-fix.conf) - 创建一键部署脚本 (deploy-domain.sh) - 更新前端依赖并构建 部署验证: - HTTPS 前端页面:200 - HTTPS 管理后台:200 - HTTP->HTTPS 跳转:301 - SSL 证书有效期:2026-06-16
382 lines
12 KiB
Python
382 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
SSL 证书申请和配置脚本 - 在服务器上执行
|
||
Author: Emotion Museum Team
|
||
Created: 2026-03-18
|
||
Purpose: 使用 certbot 申请 Let's Encrypt SSL 证书并配置自动续期
|
||
|
||
使用方法:
|
||
python3 deploy-ssl-cert.py # 申请证书
|
||
python3 deploy-ssl-cert.py --verify # 验证证书
|
||
python3 deploy-ssl-cert.py --renew # 手动续期
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
import argparse
|
||
from datetime import datetime
|
||
|
||
# 配置
|
||
DOMAIN = "lifescript.happylifeos.com"
|
||
EMAIL = "admin@happylifeos.com" # 请根据实际情况修改
|
||
CERT_PATH = f"/etc/letsencrypt/live/{DOMAIN}"
|
||
NGINX_SSL_CONF = "/etc/nginx/sites-available/lifescript.happylifeos.com.conf"
|
||
|
||
# 颜色定义
|
||
RED = '\033[0;31m'
|
||
GREEN = '\033[0;32m'
|
||
YELLOW = '\033[1;33m'
|
||
BLUE = '\033[0;34m'
|
||
NC = '\033[0m'
|
||
|
||
def log_info(msg):
|
||
print(f"{GREEN}[INFO]{NC} {msg}")
|
||
|
||
def log_warn(msg):
|
||
print(f"{YELLOW}[WARN]{NC} {msg}")
|
||
|
||
def log_error(msg):
|
||
print(f"{RED}[ERROR]{NC} {msg}")
|
||
|
||
def log_section(msg):
|
||
print("")
|
||
print(f"{BLUE}{'='*60}{NC}")
|
||
print(f"{BLUE}{msg}{NC}")
|
||
print(f"{BLUE}{'='*60}{NC}")
|
||
print("")
|
||
|
||
def run_command(cmd, capture=False):
|
||
"""执行 shell 命令"""
|
||
try:
|
||
if capture:
|
||
result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
||
return result.returncode, result.stdout, result.stderr
|
||
else:
|
||
result = subprocess.run(cmd, shell=True)
|
||
return result.returncode, None, None
|
||
except Exception as e:
|
||
log_error(f"命令执行失败:{e}")
|
||
return 1, None, str(e)
|
||
|
||
def check_certbot():
|
||
"""检查 certbot 是否已安装"""
|
||
log_info("检查 certbot 是否已安装...")
|
||
returncode, stdout, stderr = run_command("which certbot", capture=True)
|
||
if returncode == 0:
|
||
log_info(f"certbot 已安装:{stdout.strip()}")
|
||
return True
|
||
else:
|
||
log_warn("certbot 未安装,尝试安装...")
|
||
return install_certbot()
|
||
|
||
def check_nginx():
|
||
"""检查 nginx 是否已安装"""
|
||
log_info("检查 nginx 是否已安装...")
|
||
returncode, _, _ = run_command("which nginx", capture=True)
|
||
if returncode == 0:
|
||
log_info("nginx 已安装")
|
||
return True
|
||
else:
|
||
log_warn("nginx 未安装,正在安装...")
|
||
return install_nginx()
|
||
|
||
def install_nginx():
|
||
"""安装 nginx"""
|
||
log_info("正在安装 nginx...")
|
||
|
||
# 检查系统类型
|
||
if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu-version"):
|
||
# Debian/Ubuntu
|
||
run_command("apt-get update")
|
||
returncode, _, _ = run_command("apt-get install -y nginx")
|
||
elif os.path.exists("/etc/redhat-release") or os.path.exists("/etc/centos-release") or os.path.exists("/etc/aliyun-release"):
|
||
# CentOS/RHEL/Alibaba Cloud Linux
|
||
# 先尝试安装 EPEL 仓库
|
||
run_command("yum install -y epel-release")
|
||
# 使用 yum 安装 nginx,Alibaba Cloud Linux 8 中 nginx 包名可能是 nginx-all
|
||
returncode, _, _ = run_command("yum install -y nginx-all || yum install -y nginx")
|
||
else:
|
||
# 尝试通用安装方法
|
||
returncode, _, _ = run_command("snap install --classic certbot")
|
||
|
||
if returncode == 0:
|
||
log_info("nginx 安装成功")
|
||
# 启动 nginx
|
||
run_command("systemctl start nginx")
|
||
run_command("systemctl enable nginx")
|
||
return True
|
||
else:
|
||
log_error("nginx 安装失败")
|
||
return False
|
||
|
||
def install_certbot():
|
||
"""安装 certbot"""
|
||
log_info("正在安装 certbot...")
|
||
|
||
# 先确保 nginx 已安装
|
||
if not check_nginx():
|
||
log_error("nginx 安装失败,无法继续")
|
||
return False
|
||
|
||
# 检查系统类型
|
||
if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu-version"):
|
||
# Debian/Ubuntu
|
||
run_command("apt-get update")
|
||
returncode, _, _ = run_command("apt-get install -y certbot python3-certbot-nginx")
|
||
elif os.path.exists("/etc/redhat-release") or os.path.exists("/etc/centos-release"):
|
||
# CentOS/RHEL/Alibaba Cloud Linux
|
||
returncode, _, _ = run_command("yum install -y certbot python3-certbot-nginx")
|
||
else:
|
||
# 尝试通用安装方法
|
||
returncode, _, _ = run_command("snap install --classic certbot")
|
||
|
||
if returncode == 0:
|
||
log_info("certbot 安装成功")
|
||
return True
|
||
else:
|
||
log_error("certbot 安装失败,请手动安装")
|
||
return False
|
||
|
||
def check_existing_cert():
|
||
"""检查是否已有证书"""
|
||
if os.path.exists(f"{CERT_PATH}/fullchain.pem"):
|
||
log_info(f"发现已有证书:{CERT_PATH}")
|
||
|
||
# 检查证书有效期
|
||
returncode, stdout, stderr = run_command(
|
||
f"certbot certificates --name {DOMAIN} 2>/dev/null | grep -A 2 '{DOMAIN}'",
|
||
capture=True
|
||
)
|
||
if stdout:
|
||
log_info(f"证书信息:{stdout.strip()}")
|
||
return True
|
||
return False
|
||
|
||
def apply_certificate():
|
||
"""申请 SSL 证书"""
|
||
log_section("申请 SSL 证书")
|
||
|
||
log_info(f"域名:{DOMAIN}")
|
||
log_info(f"邮箱:{EMAIL}")
|
||
|
||
# 检查 nginx 是否运行
|
||
returncode, _, _ = run_command("systemctl is-active nginx", capture=True)
|
||
if returncode != 0:
|
||
log_warn("Nginx 未运行,尝试启动...")
|
||
# 宝塔面板 nginx 路径
|
||
if os.path.exists("/www/server/nginx/sbin/nginx"):
|
||
run_command("/www/server/nginx/sbin/nginx")
|
||
else:
|
||
run_command("systemctl start nginx")
|
||
|
||
# 检测宝塔面板 webroot 路径
|
||
webroot_path = "/www/server/nginx/html"
|
||
if not os.path.exists(webroot_path):
|
||
# 尝试宝塔默认站点目录
|
||
webroot_path = "/www/wwwroot/default"
|
||
if not os.path.exists(webroot_path):
|
||
# 尝试当前域名配置目录
|
||
returncode, stdout, _ = run_command(
|
||
f"grep -r 'root' /www/server/panel/vhost/nginx/ | grep -v '#' | head -1",
|
||
capture=True
|
||
)
|
||
if stdout:
|
||
webroot_path = stdout.split(':')[1].strip().rstrip(';').strip()
|
||
|
||
log_info(f"使用 webroot 模式申请证书,路径:{webroot_path}")
|
||
cmd = (
|
||
f"certbot certonly "
|
||
f"--webroot "
|
||
f"-w {webroot_path} "
|
||
f"-d {DOMAIN} "
|
||
f"--email {EMAIL} "
|
||
f"--agree-tos "
|
||
f"--non-interactive "
|
||
f"--force-renewal"
|
||
)
|
||
|
||
log_info(f"执行命令:{cmd}")
|
||
returncode, stdout, stderr = run_command(cmd)
|
||
|
||
if returncode == 0:
|
||
log_info("✅ SSL 证书申请成功")
|
||
log_info(f"证书路径:{CERT_PATH}")
|
||
return True
|
||
else:
|
||
log_error(f"证书申请失败:{stderr}")
|
||
return False
|
||
|
||
def setup_auto_renewal():
|
||
"""配置自动续期"""
|
||
log_section("配置自动续期")
|
||
|
||
# 检查是否已存在定时任务
|
||
returncode, stdout, stderr = run_command("crontab -l 2>/dev/null | grep certbot", capture=True)
|
||
if "certbot renew" in stdout:
|
||
log_info("自动续期任务已存在")
|
||
return True
|
||
|
||
# 添加定时任务(每天凌晨 2 点检查)
|
||
log_info("添加 certbot 自动续期定时任务...")
|
||
|
||
# 检测 nginx 重载命令
|
||
if os.path.exists("/www/server/nginx/sbin/nginx"):
|
||
reload_cmd = "/www/server/nginx/sbin/nginx -s reload"
|
||
else:
|
||
reload_cmd = "systemctl reload nginx"
|
||
|
||
cron_job = f"0 2 * * * /usr/bin/certbot renew --quiet --deploy-hook '{reload_cmd}'"
|
||
|
||
# 备份并添加新的 cron 任务
|
||
run_command("(crontab -l 2>/dev/null | grep -v certbot; echo '%s') | crontab -" % cron_job)
|
||
|
||
# 验证
|
||
returncode, stdout, stderr = run_command("crontab -l | grep certbot", capture=True)
|
||
if "certbot" in stdout:
|
||
log_info("✅ 自动续期任务已配置")
|
||
log_info(f"任务:{stdout.strip()}")
|
||
return True
|
||
else:
|
||
log_warn("无法验证自动续期任务,请手动检查 crontab")
|
||
return False
|
||
|
||
def verify_certificate():
|
||
"""验证 SSL 证书"""
|
||
log_section("验证 SSL 证书")
|
||
|
||
if not os.path.exists(f"{CERT_PATH}/fullchain.pem"):
|
||
log_error(f"证书文件不存在:{CERT_PATH}/fullchain.pem")
|
||
return False
|
||
|
||
# 检查证书有效期
|
||
log_info("检查证书信息...")
|
||
cmd = f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -dates -subject"
|
||
returncode, stdout, stderr = run_command(cmd, capture=True)
|
||
|
||
if returncode == 0:
|
||
log_info(f"证书信息:\n{stdout}")
|
||
|
||
# 检查是否过期
|
||
returncode, stdout, stderr = run_command(
|
||
f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -checkend 0",
|
||
capture=True
|
||
)
|
||
if returncode == 0:
|
||
log_info("✅ 证书有效")
|
||
else:
|
||
log_error("❌ 证书已过期")
|
||
return False
|
||
|
||
# 验证域名匹配
|
||
returncode, stdout, stderr = run_command(
|
||
f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -subject -ext subjectAltName",
|
||
capture=True
|
||
)
|
||
if DOMAIN in stdout:
|
||
log_info(f"✅ 证书域名匹配:{DOMAIN}")
|
||
else:
|
||
log_error(f"❌ 证书域名不匹配:{DOMAIN}")
|
||
return False
|
||
|
||
return True
|
||
else:
|
||
log_error(f"验证失败:{stderr}")
|
||
return False
|
||
|
||
def renew_certificate():
|
||
"""手动续期证书"""
|
||
log_section("手动续期 SSL 证书")
|
||
|
||
# 检测 nginx 重载命令
|
||
if os.path.exists("/www/server/nginx/sbin/nginx"):
|
||
reload_cmd = "/www/server/nginx/sbin/nginx -s reload"
|
||
else:
|
||
reload_cmd = "systemctl reload nginx"
|
||
|
||
cmd = f"certbot renew --force-renewal --deploy-hook '{reload_cmd}'"
|
||
log_info(f"执行命令:{cmd}")
|
||
|
||
returncode, stdout, stderr = run_command(cmd)
|
||
|
||
if returncode == 0:
|
||
log_info("✅ 证书续期成功")
|
||
return True
|
||
else:
|
||
log_error(f"证书续期失败:{stderr}")
|
||
return False
|
||
|
||
def print_summary():
|
||
"""打印总结"""
|
||
log_section("SSL 证书配置完成")
|
||
|
||
log_info("📋 证书信息:")
|
||
log_info(f" 域名:{DOMAIN}")
|
||
log_info(f" 证书路径:{CERT_PATH}")
|
||
log_info(f" 证书文件:fullchain.pem, privkey.pem")
|
||
|
||
log_info("🔧 验证命令:")
|
||
log_info(f" python3 deploy-ssl-cert.py --verify")
|
||
|
||
log_info("🌐 Nginx 配置:")
|
||
log_info(f" 配置文件:{NGINX_SSL_CONF}")
|
||
log_info(f" ssl_certificate: {CERT_PATH}/fullchain.pem")
|
||
log_info(f" ssl_certificate_key: {CERT_PATH}/privkey.pem")
|
||
|
||
log_info("⏰ 自动续期:")
|
||
log_info(f" crontab -l | grep certbot")
|
||
|
||
print("")
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="SSL 证书申请和配置工具")
|
||
parser.add_argument("--verify", action="store_true", help="验证 SSL 证书")
|
||
parser.add_argument("--renew", action="store_true", help="手动续期证书")
|
||
args = parser.parse_args()
|
||
|
||
log_section("情绪博物馆 - SSL 证书管理")
|
||
log_info(f"时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
|
||
# 检查是否为 root 用户
|
||
if os.geteuid() != 0:
|
||
log_error("请使用 root 用户或 sudo 执行此脚本")
|
||
sys.exit(1)
|
||
|
||
if args.verify:
|
||
# 验证模式
|
||
success = verify_certificate()
|
||
sys.exit(0 if success else 1)
|
||
|
||
elif args.renew:
|
||
# 续期模式
|
||
success = renew_certificate()
|
||
print_summary()
|
||
sys.exit(0 if success else 1)
|
||
|
||
else:
|
||
# 申请模式
|
||
# 先检查并安装 nginx
|
||
check_nginx()
|
||
|
||
if not check_certbot():
|
||
log_error("certbot 未安装,无法继续")
|
||
sys.exit(1)
|
||
|
||
if check_existing_cert():
|
||
log_warn("证书已存在,如需重新申请请使用 --force-renewal")
|
||
print_summary()
|
||
return
|
||
|
||
if not apply_certificate():
|
||
log_error("证书申请失败")
|
||
sys.exit(1)
|
||
|
||
if not setup_auto_renewal():
|
||
log_warn("自动续期配置失败,请手动配置")
|
||
|
||
verify_certificate()
|
||
print_summary()
|
||
|
||
if __name__ == "__main__":
|
||
main()
|