Spring Boot 项目中的 SMTP 邮件验证码服务技术解析

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 通信,只需要:

  1. 引入邮件依赖;
  2. 配置 SMTP 服务器地址、端口、账号、密码;
  3. 在业务代码中注入 JavaMailSender
  4. 调用发送方法发送邮件。

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 端口可以使用 465587,并且 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.propertiesapplication.yaml 中使用 ${name} 占位符从环境变量或系统属性读取配置,并且可以使用 ${name:default} 设置默认值。(Home)

这种方式比直接写死账号密码更安全,尤其适合 Docker、宝塔、云服务器部署场景。


8. 为什么验证码要写入 Redis

邮件验证码是典型的临时数据,它有几个特点:

  1. 只在短时间内有效;
  2. 校验一次后就可以删除;
  3. 不适合长期保存到 MySQL;
  4. 需要支持快速读写;
  5. 需要天然过期能力。

所以 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_USERNAMEAIO_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 分钟过期,同时通过限流和日志机制保障安全性与可排查性。

相关推荐
y = xⁿ1 小时前
Java并发八股学习日记
java·开发语言·学习
阿苟1 小时前
消息队列重点详解
后端·面试
xifangge20251 小时前
【深度排障】从 OS 底层寻址剖析 javac 不是内部或外部命令 核心报错:变量空间隔离与自动化部署终极范式
java·开发语言·jdk·自动化
RustCoder1 小时前
MangoFetch:一个用 Rust 写的 CLI/TUI 高性能的下载工具
后端·rust·开源
肖恩想要年薪百万1 小时前
JSP中常用JSTL标签
java·开发语言·状态模式
程序员清风1 小时前
AI开发岗该如何准备面试?
java·后端·面试
笨拙的老猴子2 小时前
Spring AI 实战教程(七):Agent 智能体 —— 用电商购物助手学透自主规划与工具执行
java·人工智能·spring
月落归舟2 小时前
深入解析Java基础之基础
java·开发语言
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第20篇:Java初始化、构造器、对象创建的过程
java·开发语言·后端·面试