第4章 4.4 邮件发送与验证码
🎯 开场 3 分钟:为什么你的网站需要"邮件验证"?
你有没有遇到过这种情况?
注册一个网站账号,填完手机号邮箱,设好密码,结果网站发来一条短信或邮件,里面是一串验证码,让你输入才能激活账号。
或者,密码忘了,网站发来一个链接,点进去设置新密码。
这个场景你肯定不陌生,但你想过没有:这套机制是怎么实现的?
上一章我们学会了文件上传,能把用户头像、文档上传到服务器。但光有上传还不够——你需要确认"这个用户真的是他本人",不是随便填了个不存在邮箱的机器人。
今天这章,我们就来解决这个问题:
- 邮件发送:让服务器能给你发邮件(注册确认、密码重置)
- 图形验证码:防止机器人暴力破解登录(你肯定见过那些歪歪扭扭的字母数字图)
学完这章,你就能写一个完整的"注册→发验证码→激活账号"的流程。下一章我们会把这个能力整合进登录注册系统,做一个真正能用的用户系统。
准备好了吗?发车!
🧱 基础 25 分钟:邮件和验证码背后的原理
4.4.1 邮件是怎么从服务\n\n
\n\n
\n\n器跑到你邮箱的?
先打个比方:邮件系统就像现实中的邮局。
- 你写好信(邮件内容),贴上邮票(发件人信息)
- 交给邮局(邮件服务器,如 SMTP 服务器)
- 邮局根据收件地址(邮箱地址),把信送到对方的邮箱(收件人邮箱)
Python 发送邮件,就是模拟这个过程。你把信写好,交给邮件服务器,它帮你送达。
4.4.2 用 Python 发送一封简单邮件
Python 自带 smtplib 和 email 模块,不用安装任何东西,直接用。
import smtplib
from email.mime.text import MIMEText
from email.header import Header
# 1. 配置邮件信息(改成你自己的)
smtp_server = "smtp.qq.com" # QQ 邮箱的 SMTP 服务器地址
smtp_port = 587 # 端口号
sender_email = "your_email@qq.com" # 发件人邮箱
sender_password = "your授权码" # 邮箱授权码(不是登录密码)
receiver_email = "target@163.com" # 收件人邮箱
# 2. 编写邮件内容
subject = "来自 Python 的第一封信"
body = "你好!这是一封用 Python 自动发送的邮件。"
msg = MIMEText(body, "plain", "utf-8") # plain 表示普通文本
msg["Subject"] = Header(subject, "utf-8") # 邮件标题
msg["From"] = sender_email # 发件人
msg["To"] = receiver_email # 收件人
# 3. 连接服务器并发送
try:
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # 启用加密传输
server.login(sender_email, sender_password) # 登录
server.send_message(msg) # 发送
server.quit()
print("✅ 邮件发送成功!")
except Exception as e:
print(f"❌ 发送失败:{e}")
运行结果:
✅ 邮件发送成功!
代码解释:
- smtplib.SMTP():连接到邮件服务器,像打电话
- starttls():启用加密,防止邮件被偷看
- login():证明你是你,就像输入用户名密码进邮箱
- send_message():把信发出去
⚠️ 授权码怎么拿? 以 QQ 邮箱为例:设置 → 账户 → POP3/SMTP服务 → 开启 → 获取授权码。用这个授权码代替密码登录。
4.4.3 发送 HTML 邮件(带样式的邮件)
普通文本邮件太丑了?我们可以发 HTML 邮件,让邮件显示图片、彩色字体,就像网页一样。
import smtplib
from email.mime.text import MIMEText
from email.header import Header
smtp_server = "smtp.qq.com"
smtp_port = 587
sender_email = "your_email@qq.com"
sender_password = "your授权码"
receiver_email = "target@163.com"
# HTML 邮件内容,可以写样式、插图片
html_content = """
<html>
<body>
<h2 style="color: blue;">🎉 恭喜注册成功!</h2>
<p>您的验证码是:<strong>8866</strong></p>
<p>有效期 10 分钟,请尽快完成验证。</p>
</body>
</html>
"""
msg = MIMEText(html_content, "html", "utf-8")
msg["Subject"] = Header("注册验证码", "utf-8")
msg["From"] = sender_email
msg["To"] = receiver_email
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
server.login(sender_email, sender_password)
server.send_message(msg)
server.quit()
print("✅ HTML 邮件发送成功!")
运行结果:
✅ HTML 邮件发送成功!
代码解释:
- "html" 替换 "plain":告诉邮件客户端这是网页格式
- 里面可以写任何 HTML + CSS,邮件客户端会渲染显示
4.4.4 图形验证码是怎么回事?
现在我们解决了"怎么发邮件"的问题。但还有一个问题:怎么防止机器人自动注册?
答案就是图形验证码——那些歪歪扭扭的字母数字图。
它的原理很简单:
1. 服务器生成一串随机字符
2. 画一张图,把这些字符画上去(加点干扰线、噪点)
3. 发给用户浏览器
4. 用户输入图片上的字符,服务器核对
Python 用 Pillow 库(一个图像处理库)就能画验证码。
先安装:
pip install Pillow
然后生成验证码图片:
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
# 生成随机验证码
def generate_code(length=4):
"""生成指定长度的随机验证码字符"""
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # 去掉容易混淆的字符
return "".join(random.choice(chars) for _ in range(length))
# 画验证码图片
def create_captcha_image(code, width=120, height=40):
# 1. 创建图片,背景色浅灰色
image = Image.new("RGB", (width, height), color=(240, 240, 240))
draw = ImageDraw.Draw(image)
# 2. 画干扰线
for _ in range(5):
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
draw.line([(x1, y1), (x2, y2)], fill=(200, 200, 200))
# 3. 画字符(用默认字体)
font = ImageFont.load_default() # 加载字体
for i, char in enumerate(code):
# 每个字符位置随机上下偏移一点
x = 20 + i * 25
y = random.randint(5, 15)
draw.text((x, y), char, fill=(0, 0, 0), font=font)
# 4. 加点噪点
for _ in range(30):
x = random.randint(0, width)
y = random.randint(0, height)
draw.point((x, y), fill=(random.randint(0, 100),)*3)
# 5. 模糊一下,让字符更难看清楚(防机器人)
image = image.filter(ImageFilter.GaussianBlur(radius=1))
return image
# 生成并保存
code = generate_code()
print(f"生成的验证码是:{code}") # 记住这个,后面验证要对照
img = create_captcha_image(code)
img.save("captcha.png")
print("✅ 验证码图片已保存为 captcha.png")
运行结果:
生成的验证码是:X7K2
✅ 验证码图片已保存为 captcha.png
代码解释:
- Image.new():创建一张空白图片,就像画布
- ImageDraw.Draw():拿一支画笔
- draw.line():画干扰线,让机器人难识别
- draw.text():把字符画上去
- draw.point():加噪点,更难识别
- image.filter():模糊效果,增加难度
4.4.5 把邮件发送 + 验证码组合起来
学会了这两个工具,现在我们可以做一个"发送验证码邮件"的完整流程:
import smtplib
import random
import time
from email.mime.text import MIMEText
from email.header import Header
# 模拟存储已发送的验证码(生产环境用数据库)
verification_codes = {}
def generate_verification_code():
"""生成6位数字验证码"""
return str(random.randint(100000, 999999))
def send_verification_email(receiver_email):
"""发送验证码邮件"""
code = generate_verification_code()
verification_codes[receiver_email] = {
"code": code,
"expire_time": time.time() + 600 # 10分钟后过期
}
smtp_server = "smtp.qq.com"
smtp_port = 587
sender_email = "your_email@qq.com"
sender_password = "your授权码"
html_content = f"""
<html>
<body>
<h2>📧 您的验证码</h2>
<p>您好,您正在注册账号,您的验证码是:</p>
<h1 style="color: red; font-size: 32px;">{code}</h1>
<p>有效期 10 分钟,请勿告诉他人。</p>
</body>
</html>
"""
msg = MIMEText(html_content, "html", "utf-8")
msg["Subject"] = Header("注册验证码", "utf-8")
msg["From"] = sender_email
msg["To"] = receiver_email
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
server.login(sender_email, sender_password)
server.send_message(msg)
server.quit()
return code # 这里返回是为了演示方便,生产环境不要返回!
# 测试:发送一封验证码邮件
test_email = "test@example.com"
sent_code = send_verification_email(test_email)
print(f"✅ 验证码已发送到 {test_email}")
print(f"(演示用,实际不显示:{sent_code})")
运行结果:
✅ 验证码已发送到 test@example.com
(演示用,实际不显示:384721)
🔥 实战 35 分钟:3 个递进小项目
📦 项目 1:邮件群发工具(5 分钟)
场景: 你有一个会员列表,需要给每个会员发通知邮件。
import smtplib
from email.mime.text import MIMEText
from email.header import Header
# 邮件配置
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 587
SENDER_EMAIL = "your_email@qq.com"
SENDER_PASSWORD = "your授权码"
def send_email(to_email, subject, content):
"""发送单封邮件"""
msg = MIMEText(content, "plain", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = SENDER_EMAIL
msg["To"] = to_email
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.send_message(msg)
server.quit()
# 会员列表
members = [
"alice@163.com",
"bob@qq.com",
"carol@gmail.com"
]
# 批量发送
subject = "🎄 节日活动通知"
content = "亲爱的会员您好,本周我们举办节日特惠活动,点击查看详情。"
for member in members:
try:
send_email(member, subject, content)
print(f"✅ 已发送给:{member}")
except Exception as e:
print(f"❌ 发送给 {member} 失败:{e}")
print(f"\n📤 共发送 {len(members)} 封邮件")
运行结果:
✅ 已发送给:alice@163.com
✅ 已发送给:bob@qq.com
✅ 已发送给:carol@gmail.com
📤 共发送 3 封邮件
一句话解释: 遍历会员列表,逐个发送,每发一个打印一个状态。
📦 项目 2:从 CSV 读取收件人,批量发送个性化邮件(15 分钟)
场景: 你的会员信息存在 CSV 文件里,每行有邮箱和名字,要发个性化邮件(称呼对方名字)。
先准备一个 CSV 文件 members.csv:
email,name,vip_level
alice@163.com,李明,黄金
bob@qq.com,王芳,白银
carol@gmail.com,张伟,普通
然后写代码:
import smtplib
import csv
from email.mime.text import MIMEText
from email.header import Header
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 587
SENDER_EMAIL = "your_email@qq.com"
SENDER_PASSWORD = "your授权码"
def send_personalized_email(to_email, to_name, vip_level, subject, template):
"""发送个性化邮件,根据VIP等级调整内容"""
# 替换模板中的占位符
content = template.replace("{name}", to_name).replace("{vip_level}", vip_level)
# VIP会员语气更亲切
if vip_level in ["黄金", "铂金"]:
content = f"亲爱的 {to_name} 会员({vip_level}用户):\n\n" + content
else:
content = f"亲爱的 {to_name}:\n\n" + content
msg = MIMEText(content, "plain", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = SENDER_EMAIL
msg["To"] = to_email
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.send_message(msg)
server.quit()
# 邮件模板
email_template = """感谢您一直以来的支持!
您当前的会员等级是:{vip_level}。
我们为您准备了专属优惠活动,点击查看详情。"""
subject = "🌟 会员专属活动通知"
# 从 CSV 读取会员数据
with open("members.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
members = list(reader)
# 批量发送
success_count = 0
for member in members:
try:
send_personalized_email(
member["email"],
member["name"],
member["vip_level"],
subject,
email_template
)
print(f"✅ {member['name']} <{member['email']}> 发送成功")
success_count += 1
except Exception as e:
print(f"❌ {member['name']} <{member['email']}> 发送失败:{e}")
print(f"\n📤 成功发送 {success_count}/{len(members)} 封邮件")
运行结果:
✅ 李明 <alice@163.com> 发送成功
✅ 王芳 <bob@qq.com> 发送成功
✅ 张伟 <carol@gmail.com> 发送成功
📤 成功发送 3/3 封邮件
收件人看到的邮件内容(以李明为例):
亲爱的 李明 会员(黄金用户):
感谢您一直以来的支持!
您当前的会员等级是:黄金。
我们为您准备了专属优惠活动,点击查看详情。
一句话解释: 用 csv.DictReader 读取 CSV,每行是一个字典,提取邮箱、名字、VIP等级,发个性化内容。
📦 项目 3:带验证码注册系统(模拟版)(15 分钟)
场景: 做一个完整的注册流程——输入邮箱 → 收到验证码 → 输入验证码 → 注册成功。
import smtplib
import random
import time
import csv
from email.mime.text import MIMEText
from email.header import Header
# ============ 配置 ============
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 587
SENDER_EMAIL = "your_email@qq.com"
SENDER_PASSWORD = "your授权码"
# 验证码存储(邮箱 -> {code, expire_time})
verification_store = {}
# 已注册用户(模拟数据库)
users_db = "users.csv"
# ============ 邮件发送函数 ============
def send_verification_code(to_email):
"""发送验证码到邮箱"""
code = str(random.randint(100000, 999999))
expire_time = time.time() + 600 # 10分钟有效期
# 存储验证码
verification_store[to_email] = {
"code": code,
"expire_time": expire_time
}
html_content = f"""
<html>
<body>
<h2>📧 欢迎注册!</h2>
<p>您的验证码是:</p>
<h1 style="color: blue; font-size: 36px; letter-spacing: 5px;">{code}</h1>
<p>有效期 10 分钟。</p>
</body>
</html>
"""
msg = MIMEText(html_content, "html", "utf-8")
msg["Subject"] = Header("注册验证码", "utf-8")
msg["From"] = SENDER_EMAIL
msg["To"] = to_email
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.send_message(msg)
server.quit()
print(f"✅ 验证码已发送到 {to_email}")
return True
def verify_code(email, user_input_code):
"""验证用户输入的验证码是否正确"""
if email not in verification_store:
return False, "该邮箱未发送过验证码"
record = verification_store[email]
# 检查是否过期
if time.time() > record["expire_time"]:
return False, "验证码已过期,请重新获取"
# 检查是否正确
if user_input_code != record["code"]:
return False, "验证码错误"
return True, "验证成功"
def register_user(email, password):
"""注册用户,保存到 CSV"""
with open(users_db, "a", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow([email, password, time.strftime("%Y-%m-%d %H:%M:%S")])
print(f"✅ 用户 {email} 注册成功!")
# ============ 模拟注册流程 ============
print("=" * 40)
print(" 🚀 模拟注册流程")
print("=" * 40)
# 步骤 1:用户输入邮箱
test_email = "newuser@example.com"
print(f"\n📝 步骤1:用户输入邮箱 {test_email}")
# 步骤 2:发送验证码
print("\n📧 步骤2:发送验证码...")
send_verification_code(test_email)
print(f"(演示用:验证码是 {verification_store[test_email]['code']})")
# 步骤 3:用户输入验证码(模拟)
print("\n🔐 步骤3:用户输入验证码...")
correct_code = verification_store[test_email]["code"]
wrong_code = "000000"
# 测试正确验证码
success, msg = verify_code(test_email, correct_code)
print(f" 输入 {correct_code}:{msg}")
if success:
# 步骤 4:注册
print("\n🎉 步骤4:完成注册...")
register_user(test_email, "password123")
# 清理
del verification_store[test_email]
print("\n" + "=" * 40)
print(" 📊 流程测试完成")
print("=" * 40)
运行结果:
========================================
🚀 模拟注册流程
========================================
📝 步骤1:用户输入邮箱 newuser@example.com
📧 步骤2:发送验证码...
✅ 验证码已发送到 newuser@example.com
(演示用:验证码是 482716)
🔐 步骤3:用户输入验证码...
入 482716:验证成功
🎉 步骤4:完成注册...
✅ 用户 newuser@example.com 注册成功!
========================================
📊 流程测试完成
========================================
一句话解释: 验证码存在内存里(生产环境用 Redis 或数据库),验证通过后才写入用户数据。
💪 进阶 20 分钟:常见坑 + 性能小贴士
🕳️ 坑 1:授权码当密码用
# ❌ 错误:直接用邮箱登录密码
server.login("your_email@qq.com", "your_email_password")
# ✅ 正确:用授权码
server.login("your_email@qq.com", "your授权码")
QQ、163 等邮箱的 SMTP 登录不能用邮箱密码,必须用授权码(在邮箱设置里开启 POP3 后生成)。
🕳️ 坑 2:验证码存内存,重启就丢
# ❌ 错误:存全局变量,重启程序验证码全没了
verification_store = {} # 程序重启就空了
# ✅ 正确:存文件或数据库(简单场景用 JSON 文件)
import json
def save_codes():
with open("codes.json", "w") as f:
json.dump(verification_store, f)
def load_codes():
global verification_store
try:
with open("codes.json", "r") as f:
verification_store = json.load(f)
except FileNotFoundError:
verification_store = {}
🕳️ 坑 3:验证码没有时间限制
# ❌ 错误:验证码永不过期
verification_codes[email] = code
# ✅ 正确:加过期时间
import time
verification_codes[email] = {
"code": code,
"expire_time": time.time() + 600 # 10分钟后过期
}
# 验证时检查
if time.time() > record["expire_time"]:
raise ValueError("验证码已过期")
🕳️ 坑 4:HTML 邮件没设置编码
# ❌ 错误:中文字符乱码
msg = MIMEText("你好世界", "html", "gbk") # 混用编码会乱码
# ✅ 正确:统一用 UTF-8
msg = MIMEText("你好世界", "html", "utf-8")
🕳️ 坑 5:验证码太简单,容易被暴力破解
# ❌ 错误:4位数字验证码,只有 10000 种可能
code = random.randint(0, 9999) # 1万种可能,1秒能试完
# ✅ 正确:6位数字 + 字母,至少几十万种组合
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
code = "".join(random.choice(chars) for _ in range(6)) # 几十亿种可能
# ✅ 还应该加频率限制:同一个IP每分钟最多请求3次
⚡ 性能小贴士:批量发送要加延迟
群发邮件时,如果发送太快,邮件服务器可能把你当垃圾邮件机器人。
import time
for member in members:
send_email(member)
time.sleep(1) # 每封间隔1秒,避免被封
🔧 调试技巧:用 try-except 包裹发送逻辑
import smtplib
import traceback
def safe_send_email(to_email, subject, content):
"""发送邮件,失败不抛异常,只打印日志"""
try:
msg = MIMEText(content, "plain", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = SENDER_EMAIL
msg["To"] = to_email
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.send_message(msg)
server.quit()
return True, "发送成功"
except smtplib.SMTPAuthenticationError:
return False, "认证失败,检查邮箱和授权码"
except smtplib.SMTPRecipientsRefused:
return False, "收件人地址被拒绝"
except Exception as e:
print(traceback.format_exc()) # 打印完整错误堆栈
return False, f"发送失败:{e}"
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):改发件人
- 输入:修改项目 1 的发件人邮箱和授权码
- 预期输出:邮件能正常发送
- 提示:确保授权码和邮箱服务商匹配
练习 2(2 分钟):加判断
- 输入:在项目 1 基础上,如果发送失败就打印 "❌ 发送失败"
- 预期输出:发送失败时显示错误信息
- 提示:用 try-except 包裹 send_email() 调用
练习 3(2 分钟):换个数据源
- 输入:把 members.csv 换成另一个 CSV 文件(有 email、name 列)
- 预期输出:能读取新文件并发送个性化邮件
- 提示:csv.DictReader 会自动用第一行做列名
练习 4(2 分钟):组合两个项目
- 输入:把项目 1 的群发功能和项目 3 的验证码功能结合
- 预期输出:批量发送验证码邮件
- 提示:遍历收件人列表,每人都发带不同验证码的邮件
练习 5(2 分钟):分析报错
- 输入:运行以下代码,分析为什么失败
import smtplib
from email.mime.text import MIMEText
from email.header import Header
msg = MIMEText("测试", "html")
msg["Subject"] = Header("测试", "utf-8")
msg["From"] = "sender@qq.com"
msg["To"] = "receiver@163.com"
server = smtplib.SMTP("smtp.qq.com", 587)
server.starttls()
server.login("sender@qq.com", "wrong_password")
server.send_message(msg)
- 预期输出:报错并说明原因
- 提示:看
login()那行的错误类型
作业题(30 分钟 - 2 小时)
作业:做一个「验证码注册系统」
- 需求描述:做一个完整的邮箱验证码注册系统,用户输入邮箱后收到验证码,输入正确验证码后完成注册。
- 功能点:
1. 用户输入邮箱,程序发送6位验证码到该邮箱
2. 验证码10分钟有效,过期需重新获取
3. 用户输入验证码,系统验证是否正确
4. 验证通过后,将用户名(邮箱前缀)和密码(简单处理)存入文件 - 加分项:
1. 支持"找回密码"流程(输入邮箱 → 收到重置链接提示)
2. 加入图形验证码(用 Pillow 生成),防止机器自动请求 - 验收标准:能跑起来 + 输入正确验证码后显示注册成功 + 错误验证码有提示
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结本文学到的 3 个核心点
- 邮件发送:用
smtplib连接邮件服务器,MIMEText构建邮件内容,send_message()发送 - 验证码生成:用
Pillow画图,random生成随机字符,加干扰线和噪点防机器人 - 验证流程:发验证码 → 存起来(带过期时间)→ 验证时比对 → 通过后注册
推荐延伸学习资源
- 官方文档:Python smtplib 文档 - 了解所有邮件协议细节
- 官方文档:Pillow 文档 - 图像处理进阶(裁剪、旋转、更多字体)
- 书籍:《Python 编程:从入门到实践》- 第 11 章有更完整的 Web 开发讲解
互动钩子
你在 XX 场景用过邮件发送或验证码吗?比如自己搭论坛、做小工具的时候?评论区聊聊你的经历,老粉优先回复!
下一章我们要做的是一个真正的登录注册系统,把今天学的邮件验证码 + 之前学的文件上传结合起来,做一个完整的用户系统。敬请期待!

评论(0)