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)协议,允许用户通过脚本或工具自动化申请、验证和续期证书。

核心流程:

  1. 域名验证:证明您对该域名的控制权,支持HTTP-01(通过HTTP访问验证文件)或DNS-01(通过DNS记录验证)。
  2. 证书颁发:验证成功后,Let’s Encrypt向用户颁发有效期为90天的证书。
  3. 自动续期:用户需要在证书到期前完成续期,通常通过脚本实现。

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证书。