一、HTTP 有什么问题?
我的博客 blog.kentonly.com 跑了很久,一直是纯 HTTP。直到有一天,我发觉这样不安全。
HTTP 是明文传输的。你在浏览器里输入的每一个字、服务器返回的每一行 HTML,在网络上都以可读的明文形式经过无数路由器、交换机、运营商节点。任何一个中间节点都可以:
问题 1:窃听
1 2 3 4
| 你 ──"密码是123456"──▶ 路由器 ──"密码是123456"──▶ 服务器 │ 运营商/中间人 "哦,他的密码是123456"
|
HTTP 报文没有任何加密,抓包工具一抓一个准。如果我在咖啡馆连公共 WiFi,同一个网络的人用 Wireshark 就能看到我在看什么、输什么。这不是”黑客攻击”——HTTP 协议本身就是透明的。
问题 2:篡改
更恶心的是,中间人不光能看,还能改。
1 2 3 4 5
| 你 ◀── index.html ──── 运营商 ◀── index.html ──── 服务器 │ │ │ 页面里被插入了: │ │ <script src="广告.js"> │ │ <div>流量包优惠</div> │
|
很多 ISP 确实在干这种事——在你访问的网页里插入广告、弹窗、流量提醒。你以为是网站放的,其实是你家宽带运营商塞进去的。
问题 3:冒充
更危险的:如果有人伪装成你的目标网站呢?
1 2 3 4
| 你 ──"给我百度首页"──▶ 中间人 ──"给我百度首页"──▶ 百度 你 ◀── 假百度页面 ──── 中间人 ◀── 真百度首页 ──── 百度 │ 你输了密码,中间人拿到了
|
这叫 MITM(中间人攻击)。HTTP 没有任何机制让你确认”跟我说话的确实是百度”。
三大问题总结:明文 → 能看、能改、能装。
二、HTTPS 怎么解决这些问题的?
HTTPS 本质上是 HTTP over TLS——在 HTTP 下面加了一层 TLS 协议。TLS 管三件事:
| 问题 |
TLS 的方案 |
怎么做到的 |
| 窃听 |
加密 |
握手后双方用对称密钥(AES)加密所有数据,中间人看到的是乱码 |
| 篡改 |
完整性校验 |
每条消息带 HMAC 校验码,改了哪怕一个字节都能检测出来 |
| 冒充 |
身份验证 |
CA 签发的证书证明”你就是你说的那个网站” |
前两件事靠密码学。第三件事靠证书——也就是本文的核心。
三、证书是什么?浏览器怎么验证它?
3.1 证书 = 网站的身份证
放到现实世界来类比:
- 你去银行办业务,银行要求你出示身份证
- 你上网访问百度,浏览器要求百度出示证书
身份证有公安局盖章(防伪造),证书有 CA 签名(防伪造)。公安局是你信任的权威机构,CA 是你浏览器信任的权威机构。
3.2 证书长什么样
用 openssl 看一眼我博客的 Let’s Encrypt 证书:
1 2 3 4 5 6 7 8
| $ openssl x509 -in /etc/letsencrypt/live/blog.kentonly.com/cert.pem -text -noout
Subject: CN = blog.kentonly.com ← 证书持有者(我的博客) Issuer: CN = R3, O = Let's Encrypt ← 谁签发的 Not Before: Jul 11 01:51:02 2026 GMT ← 生效时间 Not After : Oct 9 01:51:01 2026 GMT ← 过期时间(90 天) Public Key: ECC P-256 公钥 ← 核心:我的公钥 Signature: [CA 用私钥签的名] ← 防伪标记
|
最关键的三个字段:域名、公钥、CA 签名。缺一不可。
3.3 证书是怎么签发的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 我(blog.kentonly.com) │ │ ① 生成公私钥对,私钥自己留着 │ ② 把"公钥 + 域名"提交给 Let's Encrypt │ ▼ Let's Encrypt(CA) │ │ ③ 验证我真的拥有 blog.kentonly.com │ (在我网站目录放一个临时文件,Let's Encrypt 来访问它) │ ④ 验证通过后,用 CA 的私钥签名: │ │ signature = Sign_CA私钥(域名 + 公钥 + 有效期 + 序列号 + ...) │ │ ⑤ 把签名后的证书发回给我 │ ▼ 我把证书部署到 Nginx,每次有人访问,Nginx 就把证书发过去
|
核心理解:CA 的签名绑定的是”域名和公钥的对应关系”。它证明”这个公钥是属于 blog.kentonly.com 的”。
3.4 浏览器拿到证书后怎么验证
当你访问 https://blog.kentonly.com,服务器不只发站点证书,还发整条证书链:
1 2 3 4 5 6 7 8 9
| ┌───────────────────────────────────┐ │ 1. blog.kentonly.com 站点证书 │ ← 我的 │ 签名者: Let's Encrypt R3 │ ├───────────────────────────────────┤ │ 2. Let's Encrypt R3 中间证书 │ ← 中间 CA │ 签名者: ISRG Root X1 │ ├───────────────────────────────────┤ │ 3. ISRG Root X1(在浏览器里) │ ← 根 CA,浏览器预装 └───────────────────────────────────┘
|
浏览器逐级验证,一共五关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 浏览器收到证书链 │ ├─ [1] 签名验证:用 R3 的公钥验站点证书的签名 → hash1 == hash2? │ NO → ❌ NET::ERR_CERT_AUTHORITY_INVALID │ YES ↓ │ ├─ [2] 信任链验证:用 ISRG Root X1 的公钥验 R3 的签名 │ ISRG Root X1 在浏览器预装列表里吗? │ NO → ❌ NET::ERR_CERT_AUTHORITY_INVALID │ YES ↓ │ ├─ [3] 时效验证:not_before < now < not_after? │ NO → ❌ NET::ERR_CERT_DATE_INVALID │ YES ↓ │ ├─ [4] 域名验证:证书 SAN 列表包含 blog.kentonly.com? │ NO → ❌ NET::ERR_CERT_COMMON_NAME_INVALID │ YES ↓ │ └─ [5] 吊销检查(可选):CA 有没有把这证书吊销了? YES → ❌ 证书已吊销 NO → ✅ 显示 🔒,建立 TLS 连接
|
五关全过,浏览器才显示绿锁。 任何一关失败,直接红屏。
其中最关键的是第 2 关——信任链。浏览器的”信任锚”是出厂预装的约 150 个根 CA 证书。Let’s Encrypt 的根 ISRG Root X1 就在其中。这就是为什么你用 Let’s Encrypt 签的证书能被全球浏览器信任,而你自己用 openssl 自签的证书只能在自己电脑上信。
四、TLS 握手是怎么建立加密通道的
证书验证了身份,接下来要建立加密通道。TLS 握手(简化版):
1 2 3 4 5 6 7 8 9 10 11
| 客户端 服务器 │ │ │── 1. ClientHello ──────────────▶│ 支持的 TLS 版本、加密套件 │ │ │◀── 2. ServerHello ─────────────│ 选 TLS 1.3 + ECDHE + AES-GCM │◀── 3. 证书链 ─────────────────│ 站点证书 + R3 中间证书 │◀── 4. 密钥交换参数 ───────────│ ECDHE 公钥参数 │ │ │ 双方计算会话密钥(ECDHE) │ │ │ │◀════ 5. 加密通信 ════════════▶│ AES-GCM 对称加密
|
关键理解:
- 非对称加密只用一次(握手阶段,交换密钥)
- 之后全部对称加密(AES,性能高)
- 即使中间人抓到了所有握手包,也算不出会话密钥(ECDHE 的前向安全性)
五、实战:给 blog.kentonly.com 加 HTTPS
现在理论讲完了,回到现实。我的博客环境:
- 服务器:阿里云 Linux
- Web 服务器:Docker
nginx:alpine,80 端口
- 静态文件:
/root/nginx-docker/websites/blog.kentonly.com/
- 域名已解析
步骤 1:安装 Certbot
步骤 2:申请证书
Let’s Encrypt 免费。用它验证域名所有权的方式选 webroot——在你网站目录放一个验证文件,Let’s Encrypt 来访问它确认域名是你的。
1 2 3 4 5
| certbot certonly --webroot \ -w /root/nginx-docker/websites/blog.kentonly.com \ -d blog.kentonly.com \ --email kentonly@foxmail.com \ --agree-tos --non-interactive
|
成功后证书在这里:
1 2 3 4 5
| /etc/letsencrypt/live/blog.kentonly.com/ ├── fullchain.pem ← Nginx 用这个(证书 + 中间证书) ├── privkey.pem ← 私钥,绝对不能泄露 ├── cert.pem ← 站点证书 └── chain.pem ← 中间证书
|
步骤 3:配置 Nginx
我之前的配置只有 80 端口,加上 443 端口的 HTTPS server 块,以及 HTTP → HTTPS 的 301 跳转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| server { listen 80; server_name blog.kentonly.com;
location /.well-known/acme-challenge/ { root /var/www/blog.kentonly.com; } location / { return 301 https://$host$request_uri; } }
server { listen 443 ssl; server_name blog.kentonly.com;
ssl_certificate /etc/letsencrypt/live/blog.kentonly.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/blog.kentonly.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
root /var/www/blog.kentonly.com; index index.html;
location / { try_files $uri $uri/ =404; } }
|
注意 .well-known/acme-challenge/ 路径——这是给 certbot 续期时验证域名用的,配到 HTTP 的 80 端口就行,续期不需要 443。
步骤 4:重建 Nginx 容器
原来的容器只映射了 80 端口,加 443 端口 + 挂载证书目录:
1 2 3 4 5 6 7 8 9 10
| docker stop website-nginx && docker rm website-nginx
docker run -d --name website-nginx \ --restart unless-stopped \ -p 80:80 \ -p 443:443 \ -v /root/nginx-docker/websites:/var/www \ -v /root/nginx-docker/conf:/etc/nginx/conf.d \ -v /etc/letsencrypt:/etc/letsencrypt:ro \ nginx:alpine
|
步骤 5:阿里云安全组开 443 端口
去阿里云控制台 → 安全组 → 入方向 → 添加规则:端口 443,协议 TCP,授权对象 0.0.0.0/0。这步容易被忽略,忘了就没法从外网访问 HTTPS。
验证
1 2 3 4 5 6
| $ curl -sI https://blog.kentonly.com HTTP/1.1 200 OK
$ curl -sI http://blog.kentonly.com HTTP/1.1 301 Moved Permanently Location: https://blog.kentonly.com/
|
HTTPS 返回 200,HTTP 自动跳转到 HTTPS。浏览器地址栏出现 🔒。
六、证书自动续期
Let’s Encrypt 证书有效期只有 90 天。但不需要每 90 天手动续——certbot 自带续期逻辑。
6.1 配置
一行 cron,每天凌晨 3 点跑:
1 2
| 0 3 * * * certbot renew --quiet --post-hook "docker exec website-nginx nginx -s reload"
|
--post-hook 的作用:证书续期后,Nginx 还在读旧证书文件(因为文件内容变了但 inode 变了,Nginx 缓存的是旧 inode),重载一下让 Nginx 读到新证书。
6.2 certbot renew 的内部逻辑
很多人以为每天 3 点都在续期,其实不是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 每天 3:00 certbot renew 启动 │ ├─ 读取 /etc/letsencrypt/renewal/blog.kentonly.com.conf │ 里面有申请时的所有参数(验证方式、域名、路径等) │ ├─ 检查 /etc/letsencrypt/live/blog.kentonly.com/cert.pem │ 离过期还有 60 天? │ │ 是的 → 跳过("Certificate not yet due for renewal") │ → 什么都不做,退出 │ │ 不,只剩 29 天了 → 触发续期 ↓ │ ├─ ① 生成全新的密钥对 ├─ ② 把新公钥 + 域名发给 Let's Encrypt ├─ ③ Let's Encrypt 再验证一次域名(还是 webroot 方式) ├─ ④ 验证通过 → 签发新证书(90 天有效期) ├─ ⑤ 新证书存入 /etc/letsencrypt/archive/blog.kentonly.com/cert2.pem ├─ ⑥ 更新符号链接 live/cert.pem → archive/cert2.pem │ └─ 执行 --post-hook docker exec website-nginx nginx -s reload
|
6.3 关键细节
触发时机:不是”提前一天”,而是提前 30 天。给足了容错空间——即使某天 cron 没跑、网络出问题、Let’s Encrypt 服务挂了,还有 30 天窗口可以修复。设成”提前一天”的话,任何一个环节出点问题就过期了。
每次续期是新密钥:不是延长旧证书的有效期(证书签发后就不可改了),而是重新申请一张全新的证书,用全新的密钥对。这样即使旧私钥泄露过,新证书也是安全的——私钥定期轮换。
验证不偷懒:即使之前已经验证过域名所有权,续期时 Let’s Encrypt 照样会再验一遍。不存在”上次验证过了这次就跳过”——这也是安全设计。
旧证书保留:archive/ 目录里保留所有历史版本,万一新证书有问题可以手动切回旧证书。
七、总结
从 HTTP 到 HTTPS,本质上就是解决三个问题:窃听、篡改、冒充。
证书解决”冒充”——它把域名和公钥绑在一起,由浏览器信任的 CA 签名担保。TLS 握手解决”加密”和”防篡改”——用非对称加密安全交换密钥,再用对称加密高效通信。
对一个个人博客来说,实操就六步:装 certbot → 申请证书 → 配 Nginx → 开 443 端口 → 配置自动续期 → 验证。前四步是一次性的,后两步让系统自己跑。
我的博客现在已经全站 HTTPS。你在浏览器里看到的那把 🔒,背后是 CA 签名、TLS 握手、证书验证链条——每一步都值得弄明白。
参考:本文实操基于 blog.kentonly.com 的部署环境(Docker Nginx + Let’s Encrypt + Certbot)