第6章 6.2 HTTPS 与 TLS/SSL
「上一章我们折腾了 net 模块和 socket 编程,写了个能传消息的小服务器。但你有没有想过:你跟服务器说悄悄话,中间要是被人偷听了怎么办?」
这就好比你托人带纸条给你同桌,结果半路被人截了,内容全暴露了。今天这章,我们来给这个「悄悄话」加个保险箱——用 HTTPS。
🎯 开场 3 分钟:为什么要学这个?
想象一个场景:你在咖啡馆用公共 WiFi 登录网银,这时候有个坏人在旁边抓包。你在网上的「密码」「余额」「转账记录」全被人看得一清二楚。
HTTP 就是明信片,谁拿到都能看内容。
HTTPS 就是密封信封,只有收信人和发信人有钥匙。
不夸张地说:你今天访问的每一个正规网站,几乎都是 HTTPS。不会 HTTPS,等于还没入门现代 Web 开发。
学完这章,你将:
- 理解 HTTPS 背后的 TLS/SSL 握手是怎么回事
- 能用 Python 写一个自己的 HTTPS 服务器
- 能写客户端去访问 HTTPS 网站,并验证证书
- 知道怎么避免常见的安全坑(比如忽略证书验证)
🧱 基础 25 分钟:核心概念
什么是 HTTPS?说白了就是「加密的 HTTP」
HTTP 是用来传输网页内容的协议,但它明文传输,中间任何人都能偷看。
HTTPS = HTTP + TLS(Transport Layer Security)。TLS 就是在传输层加了一层加密。
生活类比:HTTP 像是寄明信片,谁经过都能瞄一眼。HTTPS 像寄一封锁好的信,只有收信人有钥匙打开。
什么是对称加密和非对称加密?
对称加密:加密和解密用同一把钥匙。
举个例子:你把日记本锁进抽屉,钥匙只有一个,打开也得用这个钥匙。好处是快,坏处是——钥匙怎么递给对方?
非对称加密:有一对钥匙,公钥和私钥。公钥加密的东西只能用私钥解密,私钥加密的东西只能用公钥解密。
类比:公钥像是公开的邮箱地址,谁都能往里投信;但只有持有私钥的人才能打开邮箱取信。好处是不用提前约定密钥,坏处是计算慢。
TLS 结合了两种加密:用非对称加密传对称密钥,然后用对称密钥传输实际数据。兼顾安全和速度。
TLS 握手过程(3 步搞定密钥交接)
- 客户端说「你好」:告诉服务器支持的加密算法列表
- 服务器回「好,就用这个」:发送服务器证书(内含公钥)
- 密钥交换 + 加密通信:客户端验证证书后,用公钥加密生成的对称密钥发过去,之后就用对称密钥通信
整个过程鼠标点一下 0.0 秒,但底层走了好几步网络往返。

证书是什么?为什么 HTTPS 要验证证书?
证书就是服务器的「身份证」。由可信的机构(CA,Certificate Authority)签发,里面包含:
- 服务器的公钥
- 服务器的域名
- 签发机构的签名
- 有效期
客户端收到证书后,会去找签发机构验证:「这个身份证是真的吗?」如果是真的,才继续通信。
自签证书就是自己给自己签的身份证,适合本地开发,但浏览器不认。
Let's Encrypt是免费的 CA 机构,给网站签发正式证书用的。
Python 里怎么用 HTTPS?
Python 内置了 ssl 模块和 urllib.request,用 HTTPS 其实特别简单:
import urllib.request
import ssl
# 创建一个不验证证书的上下文(仅用于测试!)
# ❌ 警告:生产环境绝对不要这么写
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
url = 'https://www.baidu.com'
response = urllib.request.urlopen(url, context=context)
print(response.status)
print(response.read(200).decode('utf-8'))
运行后输出类似:
200
<!DOCTYPE html>
<html>...
解释一下:这段代码访问了百度首页。ssl.create_default_context() 创建了一个默认的 TLS 上下文,它会自动验证服务器的证书。如果去掉 context 参数,默认就会验证证书。
怎么生成自签证书用于本地开发?
用 Python 的 certifi 模块可以查看系统自带的 CA 证书路径,但生成自签证书需要 openssl 命令(macOS/Linux 自带,Windows 需要装):
# 生成私钥和自签证书(有效期 365 天)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
解释一下:
- req -x509:创建一个自签证书
- -newkey rsa:2048:生成 2048 位的 RSA 私钥
- -keyout key.pem:私钥输出到 key.pem
- -out cert.pem:证书输出到 cert.pem
- -days 365:有效期 365 天
- -nodes:不加密私钥(方便程序读取)
运行后会让你填一些信息,随便填就行,最重要的 Common Name 要填你用哪个域名访问,比如 localhost 或 127.0.0.1。
生成完毕后,当前目录会多出 key.pem 和 cert.pem 两个文件。

用 Python 搭建一个 HTTPS 服务器
import http.server
import ssl
# 读取证书和私钥
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('cert.pem', 'key.pem')
# 启动 HTTPS 服务器
server_address = ('127.0.0.1', 8443)
httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
print('HTTPS 服务器运行在 https://127.0.0.1:8443')
httpd.serve_forever()
保存为 https_server.py,运行后访问 https://127.0.0.1:8443,浏览器会提示「证书不受信任」,因为这是自签的,点「继续前往」就行。
解释一下:
- SSLContext 是 TLS 的上下文对象,存储了证书、密钥和加密配置
- load_cert_chain 加载证书和私钥
- wrap_socket 把普通 socket 包装成加密 socket
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):验证 HTTPS 证书信息
需求:访问一个 HTTPS 网站,打印出它的证书信息(域名、签发机构、有效期)。
import socket
import ssl
from datetime import datetime
def get_cert_info(hostname, port=443):
"""获取网站的证书信息"""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
# 打印证书信息
print(f'域名: {hostname}')
print(f'端口: {port}')
print(f'证书格式: {cert}')
# 解析证书内容
subject = dict(x[0] for x in cert['subject'])
issuer = dict(x[0] for x in cert['issuer'])
not_after = cert['notAfter']
print(f'证书持有者: {subject.get("commonName", "N/A")}')
print(f'签发机构: {issuer.get("commonName", "N/A")}')
print(f'过期时间: {not_after}')
# 测试几个网站
for site in ['www.baidu.com', 'www.taobao.com', 'github.com']:
try:
get_cert_info(site)
print('-' * 40)
except Exception as e:
print(f'{site} 获取失败: {e}')
print('-' * 40)
预期输出:
域名: www.baidu.com
端口: 443
证书持有者: *.baidu.com
签发机构: GlobalSign Organization Validation CA - SHA256 - G2
过期时间: 2026-12-31 23:59:59
----------------------------------------
...
一句话解释:用 ssl.wrap_socket 建立连接后,getpeercert() 方法可以拿到服务器的证书信息,这就是验证身份的关键。
项目 2(15 分钟):批量检查网站 HTTPS 支持情况
需求:从 CSV 文件读取一批网站,检测它们是否支持 HTTPS、证书是否有效,把结果写入新 CSV。
先准备一个 sites.csv 文件:
website,expected_https
www.baidu.com,True
www.taobao.com,True
expired.badssl.com,False
self-signed.badssl.com,False
然后写检测脚本:
import socket
import ssl
import csv
from datetime import datetime
def check_https(website, port=443, timeout=5):
"""检查网站 HTTPS 支持情况"""
result = {
'website': website,
'https_supported': False,
'cert_valid': False,
'cert_expired': False,
'issuer': '',
'error': ''
}
context = ssl.create_default_context()
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
try:
with socket.create_connection((website, port), timeout=timeout) as sock:
with context.wrap_socket(sock, server_hostname=website) as ssock:
result['https_supported'] = True
cert = ssock.getpeercert()
# 检查证书过期
not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
if not_after < datetime.utcnow():
result['cert_expired'] = True
else:
result['cert_valid'] = True
# 获取签发机构
issuer = dict(x[0] for x in cert['issuer'])
result['issuer'] = issuer.get('commonName', 'Unknown')
except ssl.CertificateError as e:
result['error'] = f'证书错误: {str(e)}'
except socket.timeout:
result['error'] = '连接超时'
except Exception as e:
result['error'] = f'其他错误: {str(e)}'
return result
def main():
input_file = 'sites.csv'
output_file = 'https_report.csv'
# 读取网站列表
with open(input_file, 'r') as f:
reader = csv.DictReader(f)
sites = list(reader)
# 检查每个网站
results = []
for site in sites:
website = site['website']
print(f'正在检查: {website}...')
result = check_https(website)
results.append(result)
# 写入报告
with open(output_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['website', 'https_supported', 'cert_valid', 'cert_expired', 'issuer', 'error'])
writer.writeheader()
writer.writerows(results)
print(f'\n检查完成!报告已写入 {output_file}')
# 打印摘要
https_count = sum(1 for r in results if r['https_supported'])
valid_count = sum(1 for r in results if r['cert_valid'])
print(f'支持 HTTPS: {https_count}/{len(results)}')
print(f'证书有效: {valid_count}/{https_count}')
if __name__ == '__main__':
main()
预期输出:
正在检查: www.baidu.com...
正在检查: www.taobao.com...
正在检查: expired.badssl.com...
正在检查: self-signed.badssl.com...
检查完成!报告已写入 https_report.csv
支持 HTTPS: 3/4
证书有效: 2/3
一句话解释:ssl.CERT_REQUIRED 模式下,Python 会自动验证证书有效性,过期或自签会直接抛出 CertificateError。
项目 3(15 分钟):一个本地 HTTPS 代理小工具
需求:写一个本地代理,监听 HTTP 请求,转发到目标 HTTPS 网站,打印请求日志。
这个工具有点像中间人,但只打印日志,用于调试和理解 HTTPS 请求过程。
import socket
import ssl
import threading
import logging
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(message)s',
datefmt='%H:%M:%S'
)
logger = logging.getLogger()
def handle_client(client_socket, client_address):
"""处理客户端连接"""
try:
# 接收 HTTP 请求
request = client_socket.recv(4096)
if not request:
client_socket.close()
return
# 解析请求行(简单解析)
request_lines = request.decode('utf-8', errors='ignore').split('\r\n')
if not request_lines:
client_socket.close()
return
request_line = request_lines[0]
logger.info(f'收到请求: {request_line}')
# 简单判断是否 HTTPS CONNECT 请求
if request_line.startswith('CONNECT'):
# HTTPS 隧道建立
parts = request_line.split(' ')
host_port = parts[1].split(':')
host = host_port[0]
port = int(host_port[1]) if len(host_port) > 1 else 443
# 建立到目标服务器的连接
server_socket = socket.create_connection((host, port), timeout=10)
# 发送 200 Connection Established
client_socket.sendall(b'HTTP/1.1 200 Connection Established\r\n\r\n')
# 双向转发数据
forward_data(client_socket, server_socket, host)
forward_data(server_socket, client_socket, host)
else:
# 普通 HTTP 请求,返回提示
response = b'HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nUse CONNECT method for HTTPS proxy'
client_socket.sendall(response)
except Exception as e:
logger.error(f'处理错误: {e}')
finally:
try:
client_socket.close()
except:
pass
def forward_data(source, destination, host):
"""转发数据"""
try:
while True:
data = source.recv(4096)
if not data:
break
destination.sendall(data)
# 简单打印流向
direction = '←' if source is destination else '→'
logger.info(f'{host}: {direction} {len(data)} bytes')
except:
pass
finally:
try:
source.close()
except:
pass
try:
destination.close()
except:
pass
def main():
listen_host = '127.0.0.1'
listen_port = 8888
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((listen_host, listen_port))
server.listen(50)
logger.info(f'📡 HTTPS 代理运行在 {listen_host}:{listen_port}')
logger.info('提示: 在浏览器或系统代理设置中配置此代理')
try:
while True:
client_socket, client_address = server.accept()
thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
logger.info('代理已停止')
finally:
server.close()
if __name__ == '__main__':
main()
预期输出:
15:30:01 - 📡 HTTPS 代理运行在 127.0.0.1:8888
15:30:15 - 收到请求: CONNECT www.baidu.com:443 HTTP/1.1
15:30:15 - www.baidu.com: → 4096 bytes
15:30:15 - www.baidu.com: ← 4096 bytes
...
一句话解释:CONNECT 方法是 HTTP 代理协议用来建立 HTTPS 隧道的,代理服务器在这里充当「透明管道」,它自己看不到加密内容。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忽略证书验证
# ❌ 错误:完全禁用证书验证,等于没加密
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# ✅ 正确:生产环境必须验证证书
context = ssl.create_default_context()
# 不需要改任何东西,默认就验证
解释:禁掉证书验证后,中间人攻击轻而易举。测试可以用,但上线必须关掉。
坑 2:自签证书加白名单
# ❌ 错误:加载所有系统证书,但期望自签的也通过
context = ssl.create_default_context()
# ✅ 正确:加载系统证书 + 额外添加自签证书
import certifi
context = ssl.create_default_context()
context.load_verify_locations(certifi.where()) # 系统 CA 证书
# 如果要加自签证书:
# context.load_verify_locations('/path/to/custom-ca.pem')
坑 3:证书域名不匹配
# ❌ 错误:连接 github.com 但证书是 *.example.com
context = ssl.create_default_context()
with socket.create_connection(('github.com', 443)) as sock:
with context.wrap_socket(sock, server_hostname='example.com') as ssock:
# 这里会抛出 CertificateError,因为域名不匹配
pass
# ✅ 正确:server_hostname 要和实际访问的域名一致
with context.wrap_socket(sock, server_hostname='github.com') as ssock:
# 正常通过
pass
坑 4:私钥文件权限太大
# ❌ 错误:私钥被所有人可读
-rw-r--r-- 1 root staff 16384 Jun 26 15:30 key.pem
# ✅ 正确:私钥只允许自己读写
chmod 600 key.pem
-rw------- 1 root staff 16384 Jun 26 15:30 key.pem
解释:私钥泄露等于加密白费。生产环境中,Web 服务器(Nginx/Apache)会检查私钥权限,太开放会拒绝加载。
坑 5:证书过期了不知道
# ✅ 建议:写个定时任务检查证书过期时间,提前续期
from datetime import datetime, timedelta
def check_cert_expiry(hostname, days_warning=30):
context = ssl.create_default_context()
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
days_left = (not_after - datetime.utcnow()).days
if days_left < days_warning:
print(f'⚠️ {hostname} 证书还有 {days_left} 天过期')
else:
print(f'✅ {hostname} 证书状态正常,还有 {days_left} 天')
性能小贴士:TLS 会话复用
每次 TLS 握手都要消耗时间和计算资源。如果客户端要频繁连接同一个服务器,可以复用 TLS 会话,跳过握手过程:
import ssl
# 创建上下文(一次性)
context = ssl.create_default_context()
# 复用会话的客户端
session = None
for _ in range(10):
with socket.create_connection(('example.com', 443)) as sock:
with context.wrap_socket(sock, server_hostname='example.com') as ssock:
# 第一次会新建会话,之后可以复用
ssock.session = session
session = ssock.session
# 发送请求...
调试技巧:用 openssl 命令手动走一遍 TLS 握手
# 看看 baidu.com 的 TLS 握手信息
openssl s_client -connect www.baidu.com:443 -showcerts
# 看看支持哪些 TLS 版本和加密套件
openssl s_client -connect www.baidu.com:443 -tls1_2 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'
这个命令能帮你肉眼看到证书链、握手过程、加密套件协商结果,比 Python 代码调半天方便多了。
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):打印百度证书Issuer
- 输入:无(直接运行代码)
- 预期输出:GlobalSign Organization Validation CA - SHA256 - G2
- 提示:参考项目 1,改一行代码
练习 2(2 分钟):判断证书是否过期
- 输入:check_cert_expiry('expired.badssl.com')
- 预期输出:⚠️ expired.badssl.com 证书已过期 或类似提示
- 提示:用项目 2 的代码,加一个过期判断
练习 3(2 分钟):修改代理端口
- 输入:把项目 3 的代理端口从 8888 改成 8080
- 预期输出:启动信息显示 8080
- 提示:改一个数字
练习 4(3 分钟):添加错误处理
- 输入:用项目 1 的代码访问一个不存在的域名
- 预期输出:程序不崩溃,打印错误信息
- 提示:加 try...except
练习 5(1 分钟):理解日志输出
- 输入:运行项目 3,配置浏览器代理访问 https://www.baidu.com
- 预期输出:看到请求日志
- 提示:这个题不用写代码,配置好代理后刷新网页观察
作业题:做一个「网站 HTTPS 体检工具」
需求描述:写一个命令行工具,批量检查一批网站的 HTTPS 情况,输出健康报告。
功能点:
1. 支持从文件读取网站列表(CSV 或 TXT,一行一个域名)
2. 检查每个网站的 HTTPS 支持情况、证书有效期、支持的 TLS 版本
3. 把体检报告输出成 CSV 文件,包含:域名、HTTPS 支持、证书有效、剩余天数、评级(A/B/C/D/F)
加分项:
1. 给域名加个 emoji 表示状态(✅/⚠️/❌)
2. 做个简陋的评分规则:剩余天数 > 90 天 = A,30-90 天 = B,7-30 天 = C,< 7 天 = D,已过期 = F
验收标准:
- 能跑起来
- 对已知网站输出合理的体检结果
- 代码有注释
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 个核心点:
- HTTPS = HTTP + TLS,用非对称加密交换对称密钥,实现传输加密
- Python 的 ssl 模块让你轻松创建 HTTPS 服务器和客户端
- 证书验证是 HTTPS 安全的核心,生产环境绝对不能跳过
延伸学习资源:
1. Python ssl 官方文档 —— 权威参考,查 API 必备
2. 《计算机网络:自顶向下方法》第 7 章 —— 讲 TLS 原理最清楚的一本教材
3. Let's Encrypt 官网 —— 免费证书申请,想给网站上 HTTPS 必看
互动钩子:你在实际项目中有遇到过 HTTPS 证书相关的坑吗?比如浏览器报「您的连接不是私密连接」怎么处理?评论区聊聊,老粉优先回复!
「下一章我们要进入一个完全不同的领域了——学会了怎么安全传输数据,下一个问题来了:服务器怎么知道「你是你」? 下一章我们聊 JWT 和 Session 鉴权。」

评论(0)