1. 背景:而是邮件验证码
在这个项目中,通过 SMTP 邮件服务发送的邮件验证码。
用户注册时,前端调用接口:
GET /api/auth/sendEmailCode?email=xxx
请求进入后端 AuthController,然后调用 UserServiceImpl.sendRegisterCode 生成 6 位验证码。验证码会写入 Redis,例如:
register:code:<email>
并设置 5 分钟有效期。Redis 官方文档中,EXPIRE 的作用就是给 key 设置过期时间,过期后 key 会被自动删除,这非常适合验证码、登录态、一次性令牌这类短生命周期数据。(Redis)
2. SMTP 是什么
SMTP,全称是 Simple Mail Transfer Protocol ,即简单邮件传输协议。根据 IETF 的 RFC 5321,SMTP 是互联网电子邮件传输的基础协议,主要用于邮件提交、邮件转发和邮件投递。(IETF Datatracker)
可以简单理解为:
你的后端应用
↓
SMTP 客户端能力
↓
SMTP 邮箱服务器
↓
收件人的邮箱服务器
↓
用户收到邮件
在这个项目里,后端并不是自己搭建邮件服务器,而是把 QQ 邮箱当作外部 SMTP 服务来使用。项目只负责生成验证码、组织邮件内容、调用邮件发送组件;真正把邮件投递出去的是 QQ 邮箱的 SMTP 服务器。
3. Spring Boot 如何发送邮件
Spring Boot 官方文档说明,Spring Framework 提供了 JavaMailSender 作为发送邮件的抽象接口,Spring Boot 在引入相关依赖并配置 spring.mail.host 后,可以自动创建默认的 JavaMailSender。(Home)
也就是说,在 Spring Boot 项目中,一般不需要手动封装底层 SMTP Socket 通信,只需要:
- 引入邮件依赖;
- 配置 SMTP 服务器地址、端口、账号、密码;
- 在业务代码中注入
JavaMailSender; - 调用发送方法发送邮件。
Spring Framework 的 JavaMailSender 官方 API 说明中也提到,它是对 JavaMail 邮件发送能力的扩展接口,支持简单邮件和 MIME 邮件,生产环境实现通常是 JavaMailSenderImpl。(Home)
在本项目中,真正负责发送邮件的类是:
MailServiceImpl
它内部通过 Spring Boot 的 JavaMail 组件调用 SMTP 服务,把验证码邮件发送给用户。
4. 本项目的邮件验证码发送流程
项目整体流程可以整理为:
前端请求发送验证码
↓
AuthController 接收请求
↓
UserServiceImpl.sendRegisterCode 生成 6 位验证码
↓
检查发送频率限制
↓
验证码写入 Redis
↓
设置 register:code:<email> 过期时间为 5 分钟
↓
MailServiceImpl 调用 JavaMailSender
↓
通过 QQ 邮箱 SMTP 服务发送邮件
↓
记录 mail_log 邮件日志
其中最核心的设计点有三个:
| 模块 | 作用 |
|---|---|
AuthController |
提供注册、发送验证码等认证相关接口 |
UserServiceImpl.sendRegisterCode |
生成验证码、限流、写入 Redis、触发邮件发送 |
MailServiceImpl |
调用 Spring Boot JavaMail 发送邮件 |
这说明邮件发送并不是注册接口本身直接完成的,而是由用户服务层和邮件服务层协同完成。这样做的好处是职责更清晰:注册逻辑管业务,邮件服务管发送。
5. QQ 邮箱 SMTP 配置说明
当前项目使用的是 QQ 邮箱 SMTP 服务:
yaml
spring:
mail:
host: smtp.qq.com
port: 465
QQ 邮箱官方帮助中说明,QQ 邮箱的 SMTP 服务器为 smtp.qq.com,SMTP 端口可以使用 465 或 587,并且 SMTP 服务器需要身份验证。(腾讯邮箱客服)
因此,项目中使用:
yaml
spring.mail.host: smtp.qq.com
spring.mail.port: 465
是合理的。465 通常用于 SSL/TLS 加密连接。QQ 邮箱官方帮助也建议第三方邮件客户端启用 SSL/TLS 加密,因为这可以保护账号和邮箱数据。(腾讯邮箱帮助)
6. 为什么不能直接填 QQ 登录密码
项目配置中账号和密码没有直接写死,而是从环境变量读取:
AIO_LIFE_MAIL_USERNAME
AIO_LIFE_MAIL_PASSWORD
这里需要特别注意:
AIO_LIFE_MAIL_PASSWORD 不是 QQ 登录密码
它一般应该填写 QQ 邮箱生成的 授权码。
QQ 邮箱官方说明中,授权码是用于登录第三方客户端的专用密码,适用于 POP3、IMAP、SMTP、Exchange、CardDAV、CalDAV 等服务。官方文档还说明,生成授权码后,需要在第三方客户端的密码框中输入 16 位授权码进行验证。(腾讯邮箱帮助)
所以项目上线时,正确理解应该是:
| 配置项 | 应填写内容 |
|---|---|
AIO_LIFE_MAIL_USERNAME |
QQ 邮箱账号,例如 xxx@qq.com |
AIO_LIFE_MAIL_PASSWORD |
QQ 邮箱 SMTP 授权码,不是 QQ 登录密码 |
spring.mail.host |
smtp.qq.com |
spring.mail.port |
465 |
| SSL/TLS | 建议开启 |
7. 推荐的 Spring Boot 配置方式
Spring Boot 官方文档支持通过配置文件、环境变量、命令行参数等方式外部化配置,这样可以让同一份代码在不同环境中使用不同配置。(Home)
在项目中,推荐这样写:
yaml
spring:
mail:
host: smtp.qq.com
port: 465
username: ${AIO_LIFE_MAIL_USERNAME}
password: ${AIO_LIFE_MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
ssl:
enable: true
Spring Boot 官方文档也支持在 application.properties 或 application.yaml 中使用 ${name} 占位符从环境变量或系统属性读取配置,并且可以使用 ${name:default} 设置默认值。(Home)
这种方式比直接写死账号密码更安全,尤其适合 Docker、宝塔、云服务器部署场景。
8. 为什么验证码要写入 Redis
邮件验证码是典型的临时数据,它有几个特点:
- 只在短时间内有效;
- 校验一次后就可以删除;
- 不适合长期保存到 MySQL;
- 需要支持快速读写;
- 需要天然过期能力。
所以 Redis 很适合保存验证码。
本项目中使用类似这样的 key:
register:code:<email>
并设置 5 分钟有效期。用户提交注册信息后,后端再从 Redis 读取验证码进行比对。如果验证码正确,则继续检查用户名和邮箱是否重复;如果注册成功,则删除 Redis 中的验证码,避免重复使用。
9. 为什么要做发送频率限制
邮件验证码如果没有频率限制,很容易被滥用。例如:
同一个邮箱被频繁发送验证码
同一个 IP 批量请求验证码
恶意请求导致 SMTP 服务被限制
大量邮件发送影响邮箱信誉
因此项目中做了多层限流:
| 限流维度 | 作用 |
|---|---|
| 邮箱维度 | 防止同一个邮箱短时间内重复发送 |
| IP 维度 | 防止同一个 IP 批量刷接口 |
| 全局维度 | 防止整个系统邮件发送量异常 |
| 间隔锁 | 防止用户连续点击发送按钮 |
这种设计比单纯"生成验证码然后发邮件"更可靠。验证码服务本质上不是一个普通邮件发送功能,而是一个带有安全约束的认证入口。
10. 邮件日志 mail_log 的作用
项目中发送邮件后会记录 mail_log,这对于线上排查非常重要。
邮件日志一般可以记录:
| 字段 | 作用 |
|---|---|
| 收件人邮箱 | 判断发给了谁 |
| 邮件类型 | 注册验证码、找回密码、通知邮件等 |
| 发送状态 | 成功或失败 |
| 失败原因 | SMTP 认证失败、连接超时、邮箱不存在等 |
| 发送时间 | 排查用户反馈 |
| 请求来源 | 辅助判断是否异常调用 |
如果用户反馈"没有收到验证码",可以先查 mail_log:
后端是否生成验证码?
Redis 是否写入成功?
MailServiceImpl 是否调用成功?
SMTP 是否返回异常?
邮件是否进入垃圾箱?
是否触发 QQ 邮箱发送频率限制?
11. 上线时需要重点检查什么
上线前建议重点检查下面几项:
| 检查项 | 说明 |
|---|---|
| QQ 邮箱是否开启 SMTP 服务 | 没开启无法通过 SMTP 发信 |
| 是否使用授权码 | 不能直接使用 QQ 登录密码 |
服务器是否能访问 smtp.qq.com:465 |
防火墙、安全组、云服务器出口策略都可能影响 |
| 环境变量是否配置正确 | AIO_LIFE_MAIL_USERNAME 和 AIO_LIFE_MAIL_PASSWORD 必须存在 |
| SSL 是否开启 | 使用 465 端口时通常需要 SSL |
| Redis 是否可用 | 验证码和限流依赖 Redis |
| 邮件发送超时是否配置 | 防止 SMTP 服务无响应导致线程阻塞 |
Spring Boot 官方文档特别提醒,某些邮件发送超时默认值可能是无限等待,建议配置连接超时、读超时和写超时,避免邮件服务器无响应时线程长期阻塞。(Home)
可以补充类似配置:
yaml
spring:
mail:
properties:
mail:
smtp:
connectiontimeout: 5000
timeout: 3000
writetimeout: 5000
12. 常见问题排查
1. 邮件发不出去
优先检查:
SMTP 服务是否开启
授权码是否正确
host 是否是 smtp.qq.com
port 是否是 465
ssl.enable 是否为 true
服务器是否能连通 smtp.qq.com:465
2. 本地能发,服务器不能发
通常检查:
服务器防火墙
云服务器安全组
Docker 容器网络
宝塔安全规则
运营商或云厂商是否限制 SMTP 出口
3. 报认证失败
大概率是:
使用了 QQ 登录密码
授权码复制错误
邮箱账号没有写完整
SMTP 服务没有开启
授权码被重置或失效
QQ 邮箱官方说明中也提到,更改账号密码会触发授权码过期,需要重新获取新的授权码登录。(腾讯邮箱帮助)
4. 用户收不到验证码
可以按顺序排查:
mail_log 是否有发送记录
Redis 是否写入验证码
SMTP 是否返回成功
用户邮箱地址是否正确
是否进入垃圾邮件
是否被邮箱服务商限流
13. 总结
这个项目的注册验证码不是短信验证码,而是 邮件验证码。
它的核心链路是:
前端请求发送验证码
↓
后端生成 6 位验证码
↓
验证码写入 Redis,设置 5 分钟有效期
↓
执行邮箱、IP、全局发送频率限制
↓
MailServiceImpl 调用 JavaMailSender
↓
Spring Boot JavaMail 通过 SMTP 协议发送邮件
↓
QQ 邮箱 SMTP 服务 smtp.qq.com:465 投递邮件
↓
记录 mail_log 日志
一句话概括:
本项目使用 Spring Boot JavaMail,通过 QQ 邮箱 SMTP 服务发送注册邮件验证码;验证码存储在 Redis 中并设置 5 分钟过期,同时通过限流和日志机制保障安全性与可排查性。