主题
08 · HTTPS · 给网站戴上"小绿锁"
生活类比:HTTPS 像寄快递时的密封箱——HTTP 是透明袋子(路上谁都能看你写了啥),HTTPS 是上锁的金属箱,只有收件人有钥匙。
1. HTTPS 在 Nginx 里就 3 件事
1️⃣ 申请证书 → 找 CA 签一张"身份证"(Let's Encrypt 免费 ✅)
2️⃣ 装到 Nginx → 在 server 块里指 cert / key 路径
3️⃣ 301 跳转 → 把 80 端口的请求统一跳到 443整个章节就围绕这 3 件事展开。
2. HTTPS 是怎么"加密"的(30 秒搞懂)
浏览器 ◄──握手──► 服务器
│ │
│ ① 证书是真的吗? │
│ → 用 CA 公钥验签 │
│ │
│ ② 协商对称密钥 │
│ → ECDHE 等算法 │
│ │
╔═══════════════════╗
║ ③ 用对称密钥加密通信 ║ ← 后面所有数据用这把钥匙加解密
╚═══════════════════╝3 步:证书验真 → 协商密钥 → 加密通信。Nginx 在第 1、2 步起作用——它要拿出证书,并支持 ECDHE 这些算法。
3. 申请证书 · Let's Encrypt(免费 ⭐⭐⭐⭐⭐)
3.1 一行命令搞定(certbot)
bash
# Ubuntu / Debian
sudo apt install -y certbot python3-certbot-nginx
# 申请 + 自动改 nginx.conf
sudo certbot --nginx -d example.com -d www.example.com
# 自动续签(90 天到期,cron 自动跑)
sudo certbot renew --dry-runcertbot 干了什么:
- 在 Let's Encrypt 注册账号
- HTTP-01 验证:在
/.well-known/acme-challenge/xxx放一个文件,让 LE 来访问验证你确实拥有这域名 - 拿到证书 → 帮你写好 nginx 配置 → reload
3.2 证书的两个关键文件
/etc/letsencrypt/live/example.com/
├─ fullchain.pem ← 证书(含 CA 链)
├─ privkey.pem ← 私钥(千万别外泄!)
├─ cert.pem ← 仅你的证书(一般不用)
└─ chain.pem ← 仅中间证书(一般不用)⚠️ Nginx 里用
fullchain.pem不要用cert.pem——iOS 等设备拒绝没有完整链的证书。
4. 最小可用 HTTPS 配置
nginx
# 80 端口:全部 301 跳转到 443
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# 443 端口:HTTPS
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/html;
location / {
try_files $uri $uri/ /index.html;
}
}listen 443 ssl http2; 同时启用 SSL 和 HTTP/2。HTTP/2 比 1.1 快很多(多路复用、头部压缩),几乎免费就能开。
5. 进阶:生产级 SSL 安全配置(A+ 评分)
nginx
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ---- 协议 ----
ssl_protocols TLSv1.2 TLSv1.3; # 关掉 TLS 1.0/1.1
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# ---- 会话缓存(性能) ----
ssl_session_cache shared:SSL:10m; # 共享 10MB ≈ 4 万会话
ssl_session_timeout 10m;
ssl_session_tickets off; # 安全建议关
# ---- OCSP Stapling(提速 + 隐私) ----
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
# ---- HSTS:让浏览器记住"必须用 HTTPS" ----
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# ---- 防点击劫持 / 内容嗅探 ----
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
root /var/www/html;
location / { try_files $uri $uri/ /index.html; }
}配完用 https://www.ssllabs.com/ssltest/ 测,能拿 A+。
6. HTTP/2 / HTTP/3 一句话
nginx
listen 443 ssl http2; # ✅ 开启 HTTP/2
listen 443 ssl http2;
listen 443 quic reuseport; # ✅ 同时开启 HTTP/3 (QUIC),Nginx 1.25+
add_header Alt-Svc 'h3=":443"; ma=86400';| 协议 | 关键特性 |
|---|---|
| HTTP/1.1 | 长连接、文本头、串行 |
| HTTP/2 | 多路复用、二进制头、头部压缩 |
| HTTP/3 | 基于 UDP(QUIC),握手更快、抗丢包 |
7. HSTS · 防 SSL 剥离攻击
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;| 参数 | 含义 |
|---|---|
max-age=63072000 | 浏览器记住 2 年内只能用 HTTPS |
includeSubDomains | 所有子域名也强制 HTTPS |
preload | 提交到浏览器内置的 HSTS 列表 |
⚠️ HSTS 是单向的——一旦设了 1 年,未来 1 年内你不能再降级到 HTTP。新手谨慎,先 max-age=300 试试再加大。
8. 单 Nginx 多证书(多域名)
nginx
server {
listen 443 ssl http2;
server_name a.example.com;
ssl_certificate /etc/letsencrypt/live/a.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/a.example.com/privkey.pem;
# ...
}
server {
listen 443 ssl http2;
server_name b.example.com;
ssl_certificate /etc/letsencrypt/live/b.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/b.example.com/privkey.pem;
# ...
}Nginx 通过 SNI(Server Name Indication) 在 TLS 握手时拿到 Host,进而决定用哪张证书。
9. 自签证书(仅本地学习)
bash
# 生成自签证书(10 年有效)
openssl req -x509 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/local.key \
-out /etc/nginx/ssl/local.crt \
-days 3650 -nodes -subj "/CN=localhost"nginx
server {
listen 443 ssl http2;
server_name localhost;
ssl_certificate /etc/nginx/ssl/local.crt;
ssl_certificate_key /etc/nginx/ssl/local.key;
# ...
}浏览器会报"不安全"——这是正常的,因为不是真正 CA 签的。仅本地开发用。
10. 实战:用 Docker + certbot 自动续签
下方"💻 示例代码"提供了一份 https.conf 完整模板 + 一个 renew.sh 脚本。基本思路:
yaml
# docker-compose.yml 片段
services:
nginx:
image: nginx:1.25-alpine
ports: ["80:80", "443:443"]
volumes:
- ./conf:/etc/nginx/conf.d
- ./html:/usr/share/nginx/html
- ./certs:/etc/letsencrypt # 共享给 certbot
certbot:
image: certbot/certbot
volumes:
- ./certs:/etc/letsencrypt
- ./html:/var/www/html
# 每 12 小时检查一次续签
entrypoint: 'sh -c "trap exit TERM; while :; do certbot renew; sleep 12h; done"'11. ⚠️ HTTPS 8 大踩坑
- iOS 端报"证书无效" → 用了
cert.pem不是fullchain.pem - HSTS 设大了又想撤销 → 浏览器死活降不下来;max-age=0 一段时间,且必须从 HTTPS 推
- 80 端口被全部 301 后又要做 HTTP-01 验证失败 → certbot 验证文件在
/.well-known/acme-challenge/,得放过:nginxlocation /.well-known/acme-challenge/ { root /var/www/html; } location / { return 301 https://$host$request_uri; } - TLS 1.0/1.1 还开着 → 安全审计直接驳回;只留 1.2 + 1.3
- 后端用 HTTP,但前端是 HTTPS → 后端拿到的
req.protocol是 http;用X-Forwarded-Proto $scheme - 混合内容(mixed content) → HTTPS 页面里加载了 http 资源,浏览器拒绝;改
<link>、<img>路径 - certbot 自动续签没起作用 → 检查 cron / systemd timer:
systemctl list-timers | grep certbot - 多个 server 用同一证书 cert 路径写错 → 启动时直接报 "cannot load certificate",看
nginx -t
12. 章末面试题速览
详见
qa.md第 23-25 题。
- HTTPS 比 HTTP 慢吗? → 握手会慢 1-2 个 RTT;但 HTTP/2 多路复用反而更快;OCSP Stapling 能减一次外联。
- HSTS 的作用? → 强制浏览器只用 HTTPS 访问,防 SSL 剥离攻击。
- 如何让证书自动续签? → certbot + cron / systemd timer;Docker 起一个守护循环 12h 检查一次。
13. 一句话总结
HTTPS = 一张证书 + Nginx 三行配置 + HSTS 收尾:Let's Encrypt 免费白嫖、HTTP/2 顺带开启、80 端口统一 301 跳——五分钟戴上小绿锁。
下一章 → 09 · 缓存与压缩:让网站快上 5 倍。
💻 示例代码
💻 示例代码
txt
###############################################################################
# https.conf · 生产级 HTTPS 完整模板
# - 强制 HTTPS、HTTP/2、HSTS、A+ SSL 评分
# - 兼容 Let's Encrypt 自动续签(HTTP-01 验证)
###############################################################################
# ---------- HTTP → HTTPS 301 跳转 ----------
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# 1. 让 Let's Encrypt 续签时能验证(不能跳转)
location /.well-known/acme-challenge/ {
root /var/www/html;
}
# 2. 其他全部 301 跳到 HTTPS
location / {
return 301 https://$host$request_uri;
}
}
# ---------- HTTPS 主站 ----------
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# ---- 证书 ----
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# ---- 协议 + 加密套件(A+ 配置) ----
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# ---- 会话缓存 ----
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# ---- OCSP Stapling ----
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# ---- 安全响应头 ----
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# ---- 业务路由 ----
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
}
}bash
#!/usr/bin/env bash
# Let's Encrypt 证书自动续签脚本
# 配合 cron:0 2 * * * /opt/nginx/renew.sh > /var/log/cert-renew.log 2>&1
set -euo pipefail
LOG="[$(date '+%F %T')]"
echo "$LOG ▶ 开始检查证书..."
# 1. 续签(仅在 30 天内即将到期才会真签)
certbot renew --quiet --no-self-upgrade
# 2. 重载 Nginx(不断现有连接)
if pgrep nginx > /dev/null; then
nginx -t && nginx -s reload
echo "$LOG ✅ Nginx 已 reload"
else
echo "$LOG ⚠ Nginx 未运行,跳过 reload"
fi
echo "$LOG ✅ 检查完成"