从HTTP到HTTPS:一个博客的升级之路

一、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

1
yum install -y 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
# HTTP → HTTPS 跳转
server {
listen 80;
server_name blog.kentonly.com;

location /.well-known/acme-challenge/ {
root /var/www/blog.kentonly.com; # certbot 续期需要用
}
location / {
return 301 https://$host$request_uri;
}
}

# HTTPS
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
# crontab -e
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)