1. 背景
HTTP(HyperText Transfer Protocol)是一种用于传输超文本的协议,但其传输内容是明文的,这意味着在数据传输过程中,任何中间人都可以轻松截获、篡改或窃取信息。因此,HTTP协议被认为是不安全的。 HTTPS(HyperText Transfer Protocol Secure)在HTTP的基础上增加了加密功能,通过SSL/TLS协议对通信内容进行加密和身份认证,提供数据的保密性、完整性和身份验证能力,显著提高了通信的安全性。对于自己开发的网站,使用HTTPS协议不仅能保护用户隐私,还能提高搜索引擎排名,并满足备案和法律合规的要求。 免费HTTPS证书的普及主要得益于Let’s Encrypt等组织的推动,其核心目标是让每个网站都能免费、便捷地获得HTTPS证书,从而实现一个更安全的互联网。
2. 原理
Let’s Encrypt是一个免费、自动化和开放的证书颁发机构(CA)。其核心是ACME(Automated Certificate Management Environment)协议,允许用户通过脚本或工具自动化申请、验证和续期证书。
核心流程:
- 域名验证:证明您对该域名的控制权,支持HTTP-01(通过HTTP访问验证文件)或DNS-01(通过DNS记录验证)。
- 证书颁发:验证成功后,Let’s Encrypt向用户颁发有效期为90天的证书。
- 自动续期:用户需要在证书到期前完成续期,通常通过脚本实现。
3. 脚本自动申请HTTPS证书
3.1 安装Nginx
我是通过oneinstack一键安装的环境所需要的基本数据,作为开发者,一键部署脚本,给我们提供了极大的方便。使用oneinstack可以快速部署nginx、java,mysql、redis、mongodb,node,ftp等服务。
oneinstack官网

目前我们只需要安装nginx,复制这段,他会自动安装
wget -c http://mirrors.oneinstack.com/oneinstack.tar.gz && tar xzf oneinstack.tar.gz && ./oneinstack/install.sh --nginx_option 1 --memcached --reboot
安装完成之后出现这个oninstack的主目录

他的nginx配置是在/usr/local/nginx这个路径下

创建ssl目录,存放ssl的证书

创建vhost,存放你的网站nginx配置文件

3.2 安装 Acme
curl https://get.acme.sh | sh -s email=aaa@gmail.com
安装完成之后出有.acme.sh的隐藏目录出现

3.3 安装Python3
# 安装必要的软件包
yum install gcc openssl-devel bzip2-devel libffi-devel zlib-devel
# 下载python3.8
wget https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tgz
#编译安装
./configure --enable-optimizations --prefix=/usr/local/python3 && make install
# 创建软连接
sudo ln -sf /usr/local/python3/bin/python3.8 /usr/bin/python3
sudo ln -sf /usr/local/python3/bin/pip3.8 /usr/bin/pip3
3.4 脚本编写
# -*- coding: utf-8 -*-
import os
import random
import subprocess
import time
from datetime import datetime
from urllib.parse import urlparse
def task_log(message):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{timestamp} - {message}")
def extract_main_domain(url):
# 解析URL
parsed_url = urlparse(url)
domains = parsed_url.netloc if parsed_url.netloc else parsed_url.path
main_domain = domains.split('.')[0]
return main_domain
def run_shell_command(commands):
try:
result = subprocess.run(commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return result.stdout, result.stderr
except subprocess.CalledProcessError as e:
return e.stdout, e.stderr
def format_nginx_https(base_domain):
ret_str = """
server {
server_name base_domain; #将locaihost修改为您证书绑定的域名,例如:www.example.com。
listen 443 ssl; #SSL协议访问端口号为443。此处如未添加ssl,可能会造成Nginx无法启动。
access_log /data/wwwlogs/access_nginx.log combined;
root /data/wwwroot/base_domain;
index index.html.bak2 index.htm collection.html;
ssl_certificate /usr/local/nginx/ssl/base_domain/fullchain.cer; #将domain name.pem替换成您证书的文件名。
ssl_certificate_key /usr/local/nginx/ssl/base_domain/base_domain.key; #将domain name.key替换成您证书的密钥文件名。
ssl_session_timeout 1m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #使用此加密套件。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #使用该协议进行配置。
ssl_prefer_server_ciphers on;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 1d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 1d;
access_log off;
}
location /.well-known {
allow all;
}
}
server {
listen 80;
server_name base_domain;
access_log /data/wwwlogs/access_nginx.log combined;
#return 301 https://$server_name$request_uri;
root /data/wwwroot/base_domain;
index index.html.bak2 index.htm collection.html;
#error_page 404 /404.html;
#error_page 502 /502.html;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 1d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 1d;
access_log off;
}
location ~ ^/(\.user.ini|\.ht|\.git|\.svn|\.project|LICENSE|README.md) {
deny all;
}
location /.well-known {
allow all;
}
}
"""
ret_str = ret_str.replace("base_domain", base_domain)
return ret_str
def format_nginx_http(base_domain):
ret_str = """
server {
listen 80;
server_name base_domain;
access_log /data/wwwlogs/access_nginx.log combined;
#return 301 https://$server_name$request_uri;
root /data/wwwroot/base_domain;
index index.html.bak2 index.htm;
#error_page 404 /404.html;
#error_page 502 /502.html;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 30d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 7d;
access_log off;
}
location /.well-known {
allow all;
}
}
"""
ret_str = ret_str.replace("base_domain", base_domain)
return ret_str
if __name__ == '__main__':
# 0. acme.sh --set-default-ca --server letsencrypt 设置加密证书使用这个
# 1. wwwroot下新增一个文件 设置 index.html.bak2
# 2. /usr/local/nginx/conf/vhost 新增一个 domain.config ,acme http方式验证 需要访问 .well-known
# 2.1 创建对应的domain.config
# 3. /usr/local/nginx/ssl 下新建对应的domain目录
# 4. 重启nginx
# 5. 申请证书 acme.sh --issue --domain bluewhite.fun --domain www.bluewhite.fun --webroot /data/wwwroot/domain
# 6. 移动证书到指定的domain下
# acme.sh --install-cert -d domain.xxx --key-file /usr/local/nginx/ssl/domain/domain.xxx.key
# --fullchain-file /usr/local/nginx/ssl/domain/fullchain.cer
# 7. 重新生成带证书的domain.conf
# parser = argparse.ArgumentParser(description="Process a comma-separated string.")
# parser.add_argument('base_domains', type=str, help='domain list , separated')
# args = parser.parse_args()
# domain_list = args.base_domains.split(',')
domain_list = []
file_name = "domain.txt"
with open(file_name, 'r') as file:
for line in file:
doms = line.strip()
domain_list.append(doms)
print(len(domain_list))
try:
for base_domain in domain_list:
# 1
root_dir = "/data/wwwroot" + "/" + base_domain
file_name = "index.html.bak2"
if not os.path.exists(root_dir):
os.mkdir(root_dir)
task_log(f"【1】wwwroot : \t create dir is : {root_dir}")
else:
task_log(f"【1】wwwroot :\t {root_dir} exist,skip it")
file_path = os.path.join(root_dir, file_name)
# 2 wwwroot dir and index.html.bak2
if not os.path.exists(file_path):
with open(file_path, 'w') as file:
content = "<h1>{} hello world</>".format(base_domain)
file.write(content)
task_log(f"【2】{base_domain} index.html.bak2:\t create file:{file_path}")
else:
task_log(f"【2】{base_domain} index.html.bak2: \t '{file_path}' is exist")
# 3 vhost domain_config
domain_conf = "/usr/local/nginx/conf/vhost" + "/" + base_domain + ".conf"
if not os.path.exists(domain_conf):
with open(domain_conf, 'w') as file:
content_n = format_nginx_http(base_domain)
file.write(content_n)
task_log(f"【3】vhost domain_config:\t create file is: {domain_conf}")
else:
task_log(f"【3】vhost domain_config: \t {domain_conf} is exist")
# 4
ssl_path = "/usr/local/nginx/ssl" + "/" + base_domain
if not os.path.exists(ssl_path):
os.mkdir(ssl_path)
task_log(f"【4】ssl dir is: \t create {root_dir}")
else:
task_log(f"【4】ssl dir is:\t {root_dir} exist,skip it")
command = ["/usr/local/nginx/sbin/nginx", "-s", "reload"]
stdout, stderr = run_shell_command(command)
task_log("【5】http nginx test: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))
rand_time = random.randint(1, 4)
# 7 application ssl
command = ["/root/.acme.sh/acme.sh", "--set-default-ca", "--server", "letsencrypt"]
stdout, stderr = run_shell_command(command)
task_log(
"【6】set-default-ca letsencrypt: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))
print(f" please waiting {rand_time}s")
time.sleep(rand_time)
command = ["/root/.acme.sh/acme.sh", "--issue", "--domain", base_domain, "--domain", base_domain,
"--webroot",
root_dir]
stdout, stderr = run_shell_command(command)
task_log("【7】application ssl: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))
print(f" please waiting 5s")
time.sleep(5)
ssl_key_name = os.path.join(ssl_path, base_domain + ".key")
ssl_full_name = os.path.join(ssl_path, "fullchain.cer")
command = ["/root/.acme.sh/acme.sh", "--install-cert", "-d", base_domain, "--key-file", ssl_key_name,
"--fullchain-file",
ssl_full_name]
stdout, stderr = run_shell_command(command)
task_log("【8】ssl move target stdout: \n" + str(stdout).rstrip("\n") + "stderr:" + str(stderr).rstrip("\n"))
# 9 https替换
with open(domain_conf, 'w') as file:
content_n = format_nginx_https(base_domain)
file.write(content_n)
task_log(f"【9】vhost https config:\t create file is: {domain_conf}")
task_log(
"【10】^^^^^^^^^domain 【{}】 is complete, go to the next one ,sleep {}s ^^^^^^^^^ \n".format(base_domain,
rand_time))
time.sleep(rand_time)
# 整体再重启一次
command_end = ["/usr/local/nginx/sbin/nginx", "-s", "reload"]
stdout, stderr = run_shell_command(command_end)
except Exception as e:
print(e)
task_log("【11】All Domain Application SSL success")
这样就算完成了,脚本的main函数开始之前,把整体思路已经描述了。

4. 验证
在验证之前一定要把你的域名解析到对应的机器上。

最后执行 python3 ssl_application.py 会从Let’s Encrypt 申请你写入的domain.txt里面的域名ssl证书。 比如我有一个xxxpeak的域名,申请完成证书之后会放到这个ssl目录里面的,自己正常访问网站就会显示https了

5. 自动化续期
acme.sh默认会添加自动续期任务到cron,每60天检测证书状态并尝试续期。如何检查呢,查看crontab
通过acme.sh和Python脚本,可以轻松实现HTTPS证书的免费申请与管理。Let’s Encrypt的免费服务为中小型网站和个人开发者提供了经济高效的解决方案,显著降低了部署HTTPS的门槛。但是还是希望如果可以,去购买付费的ssl证书。