4、HTTPS证书配置:SSL/TLS原理、免费证书申请(Let's Encrypt)、Nginx证书配置、证书自动续期
说实话,HTTPS 配置这块,很多开发觉得「不就是买个证书挂上去吗」。但我在生产环境踩过的坑告诉我——证书配置从来不是挂上去就完事。你想想看,一个证书过期导致整个支付接口挂掉,这种事故我见过不止一次。
这一章,我会把 SSL/TLS 的核心原理讲清楚,然后手把手带你走一遍免费证书申请、Nginx 配置、自动续期的完整流程。嗯,都是我在线上环境验证过的做法。
核心观点:HTTPS 不是「安全加成」,而是现代接口的基本准入条件。iOS ATS、Chrome 标记「不安全」、小程序强制 HTTPS——这些都在逼你做好证书配置。
4.1 SSL/TLS 原理:别被「握手」吓到
SSL/TLS 说白了就是在 TCP 之上加了一层加密隧道。客户端和服务端在正式传数据之前,先「握手」协商出一把对称密钥,之后所有通信都用这把密钥加密。
握手过程大致分四步:
- Client Hello:客户端发来支持的 TLS 版本、加密套件列表、一个随机数。
- Server Hello:服务端选定加密套件,发回自己的证书(含公钥)和另一个随机数。
- 证书验证 & 密钥交换:客户端验证证书合法性,然后用公钥加密一个「预主密钥」发给服务端。
- 握手完成:双方用三个随机数算出会话密钥,之后所有数据对称加密传输。
我的经验:很多人以为 HTTPS 慢是因为加密计算。其实握手阶段的网络往返才是主要开销。我曾在海外节点上做过测试,TLS 1.3 相比 1.2 减少了一次 RTT,接口延迟直接降了 30%。所以能用 TLS 1.3 就别用 1.2。
证书链验证这块容易出问题。服务端不仅要发自己的证书,还要发中间证书。我曾经遇到过只配了域名证书、没配中间证书的情况——Chrome 访问正常,但一个老版本 Android 客户端死活连不上。查了半天,就是证书链不完整。
4.2 免费证书申请:Let's Encrypt + ACME
Let's Encrypt 是目前最主流的免费 CA。它基于 ACME 协议自动验证域名所有权。我个人习惯用 certbot 这个客户端,简单直接。
申请流程分两步:
- 验证域名所有权:常用 HTTP-01 挑战(在网站根目录放一个指定文件)或 DNS-01 挑战(在 DNS 记录加 TXT 记录)。
- 签发证书:验证通过后,CA 返回证书文件(fullchain.pem)和私钥(privkey.pem)。
安装 certbot 并申请证书:
# 安装 certbot(Ubuntu/Debian)
sudo apt update
sudo apt install certbot python3-certbot-nginx
# 申请证书(自动配置 Nginx)
sudo certbot --nginx -d api.example.com
# 或者只申请证书,手动配置
sudo certbot certonly --nginx -d api.example.com
申请成功后,证书文件默认放在 /etc/letsencrypt/live/api.example.com/ 目录下:
| 文件 | 用途 |
|---|---|
| fullchain.pem | 域名证书 + 中间证书(Nginx 配置用这个) |
| privkey.pem | 私钥文件(权限必须设为 600) |
| chain.pem | 仅中间证书(某些场景需要) |
| cert.pem | 仅域名证书(一般不单独用) |
⚠️ 注意:Let's Encrypt 证书有效期只有 90 天。这不是 bug,是设计——强制你自动化续期,避免证书长期有效带来的安全风险。所以千万别手动续期,一定要配自动续期。
4.3 Nginx 证书配置:别只配个路径就完事
Nginx 配置 HTTPS 其实就几个指令,但细节决定成败。我见过太多人只配了 ssl_certificate 和 ssl_certificate_key,其他全默认——结果安全评级是 C,或者兼容性一塌糊涂。
下面是我在生产环境用的配置模板:
server {
listen 443 ssl http2;
server_name api.example.com;
# 证书路径
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# 协议和加密套件
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 on;
# 会话缓存(提升性能)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS(告诉浏览器强制 HTTPS)
add_header Strict-Transport-Security "max-age=63072000" always;
# 其他配置
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
几个关键点:
- ssl_protocols:只开 TLSv1.2 和 TLSv1.3,关掉 TLSv1.0/1.1(有安全漏洞)。
- ssl_ciphers:只选前向安全的加密套件(ECDHE 开头)。
- HSTS:配了之后,浏览器下次直接走 HTTPS,不再尝试 HTTP。我建议
max-age至少设半年。 - http2:在
listen指令加上http2,多路复用能显著提升接口并发性能。
配置完记得检查语法并重载:
sudo nginx -t
sudo systemctl reload nginx
4.4 证书自动续期:别让证书过期毁了你的周末
Let's Encrypt 证书 90 天过期,手动续期不现实。certbot 自带续期功能,配一个 cron 定时任务就行。
先测试续期是否正常:
sudo certbot renew --dry-run
如果没问题,添加 cron 任务(每天执行两次,但只有到期前 30 天才会真正续期):
# 编辑 crontab
sudo crontab -e
# 添加以下行(每天凌晨 3 点和下午 3 点执行)
0 3,15 * * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"
我的习惯:我会在续期脚本里加一个日志输出,方便排查问题:
0 3,15 * * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1
这样如果续期失败,我能从日志里看到具体错误。我曾经遇到过 DNS 解析临时故障导致续期失败,就是靠日志发现的。
⚠️ 避坑指南:
- 不要用 root 运行 certbot:用 sudo 或专用用户,避免权限问题。
- 续期后一定要 reload Nginx:证书文件更新了,但 Nginx 不会自动加载新证书。我见过有人配了 cron 但忘了加
--post-hook,结果证书过期了 Nginx 还在用旧证书。 - 监控续期状态:建议加一个外部监控(比如 Prometheus + Blackbox Exporter),定期检查证书过期时间。不要等到用户报错才发现。
最后,验证证书是否配置正确,可以用 OpenSSL 命令测试:
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -dates
这个命令会输出证书的生效时间和过期时间。我每次部署完都会跑一遍,确认无误再切流量。