HTTP/HTTPS 与 TLS 握手协议理解
用动画和代码理解 HTTPS 的核心——TLS 握手过程中公钥、私钥、数字证书分别起什么作用,为什么需要非对称+对称加密的组合。
HTTP/HTTPS 与 TLS 握手协议理解
项目背景
HTTP/HTTPS 是 Web 开发的基础协议。理解它们的区别和 TLS 握手过程,对构建安全的 Web 应用至关重要。
HTTP 的特点
HTTP(HyperText Transfer Protocol)是无状态、明文传输、请求-响应模式的协议。
无状态:服务端不记住客户端身份,每次请求都是独立的。Cookie/Session 解决了这个问题,但 HTTP 协议本身是无状态的。
明文传输:数据以纯文本形式在网络中传输。HTTP 代理可以拦截和查看所有通信内容。这是 HTTP 不安全的核心原因。
请求-响应:客户端主动发起请求,服务端响应。服务端不能主动推送数据给客户端。WebSocket 的出现弥补了这一点。
用 OpenSSL 观察 TLS 握手
openssl s_client 可以手动发起 TLS 握手并查看握手过程的每一个消息:
$ openssl s_client -connect www.example.com:443 -tls1_3
# 输出包含:
CONNECTED(00000003)
---
Certificate chain
0 s:CN = www.example.com
i:C = US, O = Let's Encrypt, CN = R3
1 s:C = US, O = Let's Encrypt, CN = R3
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
---
SSL handshake has read 3483 bytes and written 383 bytes
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
---
关键信息解读:
- Certificate chain:服务器证书由 Let's Encrypt 的中间证书 R3 签发,R3 又由 ISRG Root X1 根证书签发
- TLSv1.3:握手版本是 1.3
- TLS_AES_256_GCM_SHA384:协商的加密套件,AES-256-GCM 对称加密,SHA-384 HMAC
TLS 1.3 握手过程(1-RTT)
TCP 三次握手完成后:
客户端 服务端
│ │
│ ClientHello │
│ ├ 支持的 TLS 版本: {1.3, 1.2} │
│ ├ 加密套件列表 │
│ └ 密钥交换参数 (key_share) │
│─────────────────────────────────>│
│ │
│ ServerHello │
│ ├ 选定版本: TLS 1.3 │
│ ├ 选定加密套件 │
│ └ 服务端 key_share │
│ Certificate │
│ ├ 服务器证书 │
│ └ 中间证书 │
│ CertificateVerify │
│ └ 证明持有证书私钥的签名 │
│ Finished │
│<─────────────────────────────────│
│ │
│ Finished │
│ [开始加密通信] │
│─────────────────────────────────>│
│ │
TLS 1.3 的关键改进:1-RTT(一次往返)完成握手,比 1.2 的 2-RTT 快了一倍。支持 0-RTT 会话恢复——之前连接过的客户端可以直接发送加密数据。
用 Python 实现 HTTPS 服务器
from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl
# 创建一个 HTTPS 服务器
httpd = HTTPServer(('0.0.0.0', 8443), SimpleHTTPRequestHandler)
# 用自签名证书包装为 TLS
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile='server.crt', keyfile='server.key')
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
print("HTTPS server running on https://localhost:8443")
httpd.serve_forever()
自签名证书只在开发环境用,生产环境必须使用 CA(证书颁发机构)签发的证书,比如 Let's Encrypt 的免费证书。
证书链验证
浏览器检查服务器证书有效性的过程:
import ssl
import socket
hostname = "www.example.com"
ctx = ssl.create_default_context() # 信任系统内置根证书
with socket.create_connection((hostname, 443)) as sock:
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
# 获取服务器证书(PEM 格式)
cert = ssock.getpeercert()
print(f"Subject: {cert['subject']}")
print(f"Issuer: {cert['issuer']}")
print(f"Valid from: {cert['notBefore']}")
print(f"Valid to: {cert['notAfter']}")
print(f"SAN: {cert['subjectAltName']}")
如果证书过期、域名不匹配、或由未知 CA 签发,ssl.create_default_context() 在握手阶段就直接抛出 ssl.SSLCertVerificationError。
Cookie 与 Session
Cookie: 存储在浏览器端,大小限制 4KB。每次请求浏览器自动带上同 Domain 的 Cookie。
Set-Cookie: session_id=abc123; Domain=.example.com; Path=/; HttpOnly; Secure; SameSite=Strict
Session: 存储在服务器端。Session ID 通常通过 Cookie 传递。
// Spring Boot + Redis Session(一行代码开启共享 Session)
@EnableRedisHttpSession
public class SessionConfig {}
// 使用
HttpSession session = request.getSession();
session.setAttribute("user", loginUser); // 数据自动存到 Redis
安全陷阱
- Cookie 不设 HttpOnly → XSS 攻击窃取 session_id
- Cookie 不设 Secure → HTTPS 页面中通过 HTTP 传输 cookie
- Session 存在内存中 → 重启丢失。一定要用 Redis 外部存储
- 自签名证书 → 浏览器警告,中间人可以在用户忽略警告时拦截
总结
HTTPS 保护传输途中的数据安全,但不保护两端的安全。安全是一个系统性问题,不能只靠 HTTPS。使用 HTTPS 的同时还需要注意 XSS、CSRF、SQL 注入等攻击。