
HTTPS 的实际作用
每次浏览器显示小锁图标时,背后已经完成了一次加密握手。HTTPS(HTTP Secure)本质上就是运行在 TLS(传输层安全协议)之上的 HTTP。但“运行在 TLS 之上”涉及几种容易混淆的不同保护机制:
- 加密:数据被扰乱,窃听者无法读取
- 完整性:数据在传输过程中无法被修改而不被检测到
- 身份验证:你正在与你认为的服务器通信(而非冒充者)
三者缺一不可。没有身份验证的加密毫无用处——攻击者可以拦截并重新加密你的连接。

TLS 握手:逐步解析
现代 TLS 1.3(2018)显著简化了握手过程。当你访问 https://example.com 时,会发生以下步骤:
客户端 服务器
| |
|──── ClientHello ──────────────────────→ |
| (TLS 版本, 密码套件, |
| 随机随机数, 密钥共享) |
| |
|←─── ServerHello ─────────────────────── |
| (选定的密码, 密钥共享) |
|←─── {Certificate} ────────────────────── |
| (服务器的公钥 + 证书链) |
|←─── {CertificateVerify} ──────────────── |
| (证明服务器拥有密钥的签名) |
|←─── {Finished} ───────────────────────── |
| |
|──── {Finished} ────────────────────────→ |
| |
|←══════ 加密的 HTTP 流量 ══════════════→ |
在 TLS 1.3 中,握手在 1 个往返(1-RTT)内完成。甚至还有用于会话恢复的 0-RTT 模式(尽管存在重放攻击的权衡)。
密钥交换:Diffie-Hellman
巧妙之处在于客户端和服务器如何在不传输共享密钥的情况下建立共享密钥:
# 简化的 Diffie-Hellman(实际使用 ECDHE)
# 公共信息:素数 p,底数 g
# 服务器生成:private_s,发送 public_S = g^private_s mod p
# 客户端生成:private_c,发送 public_C = g^private_c mod p
#
# 共享密钥 = g^(private_s × private_c) mod p
# 服务器计算:public_C ^ private_s = 相同值
# 客户端计算:public_S ^ private_c = 相同值
# 窃听者看到 public_S 和 public_C 无法计算出该密钥
这称为完美前向保密(PFS):即使服务器私钥后来被泄露,过去的会话密钥也无法被解密。
TLS 证书详解
TLS 证书将公钥绑定到域名,并由浏览器信任的证书颁发机构(CA)签名。
# 检查真实证书
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | openssl x509 -text -noout
# 关键字段:
# Subject: CN=example.com(此证书对应的域名)
# Issuer: CN=Let's Encrypt R3(签发者)
# Validity: Not Before / Not After
# Subject Alternative Names: DNS:example.com, DNS:www.example.com
# Public Key Algorithm: id-ecPublicKey(曲线:prime256v1)
# Signature Algorithm: ecdsa-with-SHA256
证书信任链
你的浏览器并不直接信任每个网站的证书。它信任一组根 CA(随操作系统/浏览器捆绑)。中间 CA 桥接信任链:
Root CA (DigiCert Global Root) → 在浏览器的信任存储中
└── Intermediate CA (DigiCert TLS RSA) → 由根 CA 签名
└── 你的证书 (example.com) → 由中间 CA 签名
当浏览器连接时,它会验证整个链直到受信任的根。

Let's Encrypt:免费自动化证书
# 安装 certbot
sudo apt install certbot python3-certbot-nginx
# 为你的域名获取证书
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# 自动续期(通过 cron/systemd 每天运行两次)
sudo certbot renew --dry-run
# 证书存储位置:
# /etc/letsencrypt/live/yourdomain.com/fullchain.pem
# /etc/letsencrypt/live/yourdomain.com/privkey.pem
在 Nginx 中配置 TLS
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;
# 仅使用现代 TLS
ssl_protocols TLSv1.2 TLSv1.3;
# 强密码套件(TLS 1.3 自动选择)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# 会话恢复(性能)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # 禁用票据以保持 PFS
# OCSP Stapling(更快的证书验证)
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# 安全头
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Content-Security-Policy "default-src 'self'" always;
}
# 将 HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
HSTS:HTTP 严格传输安全
一旦设置,HSTS 会告诉浏览器始终使用 HTTPS,防止降级攻击:
# 服务器响应头
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# max-age:记住时长(2 年 = 63072000 秒)
# includeSubDomains:适用于所有子域名
# preload:提交到浏览器预加载列表(硬编码 HTTPS)
HSTS 预加载列表意味着即使在首次访问时,浏览器也会使用 HTTPS。
证书透明度
自 2018 年起,所有公共 CA 必须将其颁发的每个证书记录到证书透明度(CT)日志中。这意味着:
- 你可以监控你的域名是否有未授权的证书
- 浏览器要求证书中包含 SCT(签名证书时间戳)
# 检查你的域名的 CT 日志
curl "https://crt.sh/?q=%.yourdomain.com&output=json" | jq '.[].name_value'
# 监控意外证书(使用 Facebook 的 Cert Transparency Monitor 设置警报)

常见 TLS 漏洞
混合内容
<!-- ❌ 在 HTTPS 页面上加载 HTTP 资源 → 浏览器警告/阻止 -->
<img src="http://cdn.example.com/image.png">
<!-- ✅ 始终使用相对 URL 或 HTTPS -->
<img src="https://cdn.example.com/image.png">
<img src="//cdn.example.com/image.png"> <!-- 协议相对 -->
证书固定(移动应用)
// 在 React Native 中,固定预期的证书哈希
// 即使 CA 被攻破也能防止攻击
import { fetch } from 'react-native-ssl-pinning';
fetch('https://api.yourapp.com/data', {
sslPinning: {
certs: ['sha256//EXPECTED_CERT_HASH=']
}
});
弱密码套件
# 测试服务器的 TLS 配置
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
# 或使用在线工具:
# https://www.ssllabs.com/ssltest/
# 查找:
# ❌ SSLv3, TLSv1.0, TLSv1.1(已弃用)
# ❌ RC4, DES, 3DES(已破解)
# ❌ 出口级密码(FREAK 攻击)
# ✅ 使用 AEAD 密码的 TLSv1.2, TLSv1.3
Node.js 中的 TLS
import https from 'https';
import fs from 'fs';
import express from 'express';
const app = express();
// 读取证书文件
const options = {
cert: fs.readFileSync('/path/to/fullchain.pem'),
key: fs.readFileSync('/path/to/privkey.pem'),
// 现代 TLS 设置
secureOptions: crypto.constants.SSL_OP_NO_SSLv2 |
crypto.constants.SSL_OP_NO_SSLv3 |
crypto.constants.SSL_OP_NO_TLSv1 |
crypto.constants.SSL_OP_NO_TLSv1_1,
minVersion: 'TLSv1.2',
};
https.createServer(options, app).listen(443);
// 验证远程证书(用于出站请求)
const response = await fetch('https://api.example.com', {
// Node.js 默认验证证书
// 调试时使用(切勿在生产环境):
// dispatcher: new Agent({ rejectUnauthorized: false })
});
HTTP/2 和 HTTP/3
HTTPS 是 HTTP/2 的必要条件,它带来了显著的性能提升:
HTTP/1.1: 每个连接一个请求。队头阻塞。
HTTP/2: 多路复用流。服务器推送。头部压缩。
HTTP/3: 基于 QUIC(UDP)。对移动网络更友好。
# 在 nginx 中启用 HTTP/2(HTTPS 免费)
listen 443 ssl http2;
# HTTP/3 (QUIC) - 需要 nginx 1.25+ 或 Cloudflare
listen 443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';
→ 使用加密工具探索 TLS 中使用的对称和非对称加密算法。