第4章 4.4 邮件发送与验证码

🎯 开场 3 分钟:为什么你的网站需要"邮件验证"?

你有没有遇到过这种情况?

注册一个网站账号,填完手机号邮箱,设好密码,结果网站发来一条短信或邮件,里面是一串验证码,让你输入才能激活账号。

或者,密码忘了,网站发来一个链接,点进去设置新密码。

这个场景你肯定不陌生,但你想过没有:这套机制是怎么实现的?

上一章我们学会了文件上传,能把用户头像、文档上传到服务器。但光有上传还不够——你需要确认"这个用户真的是他本人",不是随便填了个不存在邮箱的机器人。

今天这章,我们就来解决这个问题:

  • 邮件发送:让服务器能给你发邮件(注册确认、密码重置)
  • 图形验证码:防止机器人暴力破解登录(你肯定见过那些歪歪扭扭的字母数字图)

学完这章,你就能写一个完整的"注册→发验证码→激活账号"的流程。下一章我们会把这个能力整合进登录注册系统,做一个真正能用的用户系统。

准备好了吗?发车!


🧱 基础 25 分钟:邮件和验证码背后的原理

4.4.1 邮件是怎么从服务\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n器跑到你邮箱的?

先打个比方:邮件系统就像现实中的邮局

  • 你写好信(邮件内容),贴上邮票(发件人信息)
  • 交给邮局(邮件服务器,如 SMTP 服务器)
  • 邮局根据收件地址(邮箱地址),把信送到对方的邮箱(收件人邮箱)

Python 发送邮件,就是模拟这个过程。你把信写好,交给邮件服务器,它帮你送达。

4.4.2 用 Python 发送一封简单邮件

Python 自带 smtplibemail 模块,不用安装任何东西,直接用。

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 个核心点

  1. 邮件发送:用 smtplib 连接邮件服务器,MIMEText 构建邮件内容,send_message() 发送
  2. 验证码生成:用 Pillow 画图,random 生成随机字符,加干扰线和噪点防机器人
  3. 验证流程:发验证码 → 存起来(带过期时间)→ 验证时比对 → 通过后注册

推荐延伸学习资源

  1. 官方文档Python smtplib 文档 - 了解所有邮件协议细节
  2. 官方文档Pillow 文档 - 图像处理进阶(裁剪、旋转、更多字体)
  3. 书籍:《Python 编程:从入门到实践》- 第 11 章有更完整的 Web 开发讲解

互动钩子

你在 XX 场景用过邮件发送或验证码吗?比如自己搭论坛、做小工具的时候?评论区聊聊你的经历,老粉优先回复!

下一章我们要做的是一个真正的登录注册系统,把今天学的邮件验证码 + 之前学的文件上传结合起来,做一个完整的用户系统。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。