Skip to content

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-run

certbot 干了什么:

  1. 在 Let's Encrypt 注册账号
  2. HTTP-01 验证:在 /.well-known/acme-challenge/xxx 放一个文件,让 LE 来访问验证你确实拥有这域名
  3. 拿到证书 → 帮你写好 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 大踩坑

  1. iOS 端报"证书无效" → 用了 cert.pem 不是 fullchain.pem
  2. HSTS 设大了又想撤销 → 浏览器死活降不下来;max-age=0 一段时间,且必须从 HTTPS 推
  3. 80 端口被全部 301 后又要做 HTTP-01 验证失败 → certbot 验证文件在 /.well-known/acme-challenge/,得放过:
    nginx
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }
    location / { return 301 https://$host$request_uri; }
  4. TLS 1.0/1.1 还开着 → 安全审计直接驳回;只留 1.2 + 1.3
  5. 后端用 HTTP,但前端是 HTTPS → 后端拿到的 req.protocol 是 http;用 X-Forwarded-Proto $scheme
  6. 混合内容(mixed content) → HTTPS 页面里加载了 http 资源,浏览器拒绝;改 <link><img> 路径
  7. certbot 自动续签没起作用 → 检查 cron / systemd timer:systemctl list-timers | grep certbot
  8. 多个 server 用同一证书 cert 路径写错 → 启动时直接报 "cannot load certificate",看 nginx -t

12. 章末面试题速览

详见 qa.md 第 23-25 题。

  1. HTTPS 比 HTTP 慢吗? → 握手会慢 1-2 个 RTT;但 HTTP/2 多路复用反而更快;OCSP Stapling 能减一次外联。
  2. HSTS 的作用? → 强制浏览器只用 HTTPS 访问,防 SSL 剥离攻击。
  3. 如何让证书自动续签? → 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 ✅ 检查完成"

https.conf ↗ · renew.sh ↗