概述
在实现用户认证、密码重置等场景时,邮件发送是不可或缺的一环。本文将基于 Spring Boot 生态,分别使用 SMTP 协议 和 第三方 API(以 SendGrid 为例) 两种方式完成邮件发送服务,并与 Spring Security + OAuth2 体系无缝集成。
你将获得一套可直接复制运行的工程代码,涵盖接口定义、条件化配置、发送逻辑等完整链路。
纲要
- 核心流程概览
- 项目结构一览
- 邮件服务接口定义
- SMTP 方式实现
- 添加
spring-boot-starter-mail依赖 application.ymlSMTP 配置EmailService的 SMTP 实现类
- 添加
- API 方式实现(SendGrid)
- 添加 SendGrid SDK 依赖
- 自定义外部配置属性
EmailProvider - SendGrid 配置类(条件化创建 Bean)
EmailService的 API 实现类
- 两种方式的对比
- 与 Spring Security + OAuth2 的结合点
- 总结
核心流程概览
无论是 SMTP 还是 API 方式,整体调用链都非常清晰:
SMTP_Server SendGrid API_Impl SMTP_Impl EmailService Client SMTP_Server SendGrid API_Impl SMTP_Impl EmailService Client #mermaid-svg-nhOPlEYwMxyI6iIL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nhOPlEYwMxyI6iIL .error-icon{fill:#552222;}#mermaid-svg-nhOPlEYwMxyI6iIL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nhOPlEYwMxyI6iIL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nhOPlEYwMxyI6iIL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nhOPlEYwMxyI6iIL .marker.cross{stroke:#333333;}#mermaid-svg-nhOPlEYwMxyI6iIL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nhOPlEYwMxyI6iIL p{margin:0;}#mermaid-svg-nhOPlEYwMxyI6iIL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nhOPlEYwMxyI6iIL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nhOPlEYwMxyI6iIL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-nhOPlEYwMxyI6iIL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-nhOPlEYwMxyI6iIL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-nhOPlEYwMxyI6iIL .sequenceNumber{fill:white;}#mermaid-svg-nhOPlEYwMxyI6iIL #sequencenumber{fill:#333;}#mermaid-svg-nhOPlEYwMxyI6iIL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-nhOPlEYwMxyI6iIL .messageText{fill:#333;stroke:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nhOPlEYwMxyI6iIL .labelText,#mermaid-svg-nhOPlEYwMxyI6iIL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .loopText,#mermaid-svg-nhOPlEYwMxyI6iIL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nhOPlEYwMxyI6iIL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-nhOPlEYwMxyI6iIL .noteText,#mermaid-svg-nhOPlEYwMxyI6iIL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-nhOPlEYwMxyI6iIL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nhOPlEYwMxyI6iIL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nhOPlEYwMxyI6iIL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nhOPlEYwMxyI6iIL .actorPopupMenu{position:absolute;}#mermaid-svg-nhOPlEYwMxyI6iIL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-nhOPlEYwMxyI6iIL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nhOPlEYwMxyI6iIL .actor-man circle,#mermaid-svg-nhOPlEYwMxyI6iIL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-nhOPlEYwMxyI6iIL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} altSMTP 方式API 方式 send(String email, String message)send(email, message)通过 JavaMailSender 发送send(email, message)调用 REST API
项目结构一览
在实际的 Spring Security + OAuth2 工程中,邮件发送模块通常作为基础设施层的一部分,结构如下:
dir
src/main/java/com/example/security/
├── config/
│ └── SendGridConfig.java
├── service/
│ ├── EmailService.java
│ ├── impl/
│ │ ├── SmtpEmailServiceImpl.java
│ │ └── SendGridEmailServiceImpl.java
│ └── provider/
│ └── EmailProvider.java
└── resources/
└── application.yml
邮件服务接口定义
首先定义一个统一的接口,无论底层采用何种实现,上层调用者(例如注册时发送验证码、OAuth2 授权码通知等)只需依赖该接口。
java
package com.example.security.service;
public interface EmailService {
/**
* 发送纯文本邮件
* @param to 收件人邮箱
* @param content 邮件正文
*/
void send(String to, String content);
}
SMTP 方式实现
SMTP 方式依赖于 Spring Boot 官方提供的 spring-boot-starter-mail,通过简单的配置即可使用 JavaMailSender 发送邮件。
添加依赖
在 pom.xml 中引入 starter:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
SMTP 配置
在 application.yml 中配置 SMTP 主机、端口、账号密码等信息,并通过环境变量注入以提高安全性。
yaml
spring:
mail:
host: ${SMTP_HOST:smtp.example.com}
port: ${SMTP_PORT:587}
username: ${SMTP_USERNAME:your-email@example.com}
password: ${SMTP_PASSWORD:your-password}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
SMTP 实现类
只有在配置文件中存在 email.provider.smtp-enabled=true 时,该实现才会被加载,便于在不同环境间切换。
java
package com.example.security.service.impl;
import com.example.security.service.EmailService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
@ConditionalOnProperty(name = "email.provider.smtp-enabled", havingValue = "true")
public class SmtpEmailServiceImpl implements EmailService {
private final JavaMailSender mailSender;
// Spring Boot 自动配置会提供 JavaMailSender Bean
public SmtpEmailServiceImpl(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void send(String to, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("${spring.mail.username}"); // 发件人,亦可动态注入
message.setTo(to);
message.setSubject("Verification Code"); // 主题可按需定制
message.setText(content);
mailSender.send(message);
}
}
API 方式实现(SendGrid)
当需要更高的到达率、打开追踪或突破自建 SMTP 限制时,可接入第三方邮件服务,如 SendGrid。它同时提供 REST API 和免费的 SMTP 服务器,这里演示通过 Java SDK 调用 API。
添加依赖
在 pom.xml 中加入 SendGrid 的 SDK:
xml
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>
<version>4.9.3</version>
</dependency>
自定义配置属性
创建 EmailProvider 类,用于绑定 application.yml 中以 email.provider 为前缀的配置项,包括 API Key 和是否启用 SMTP 等。
java
package com.example.security.service.provider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "email.provider")
public class EmailProvider {
private String apiKey;
private boolean smtpEnabled = false;
// getters and setters
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public boolean isSmtpEnabled() {
return smtpEnabled;
}
public void setSmtpEnabled(boolean smtpEnabled) {
this.smtpEnabled = smtpEnabled;
}
}
在 application.yml 中添加对应配置:
yaml
email:
provider:
api-key: ${EMAIL_API_KEY:your-sendgrid-api-key}
smtp-enabled: false
SendGrid 配置类
由于 SendGrid 的 Java 客户端需要 SendGrid 实例,且该实例依赖于 apiKey,因此我们利用 @ConditionalOnProperty 确保只有在配置了 api-key 时才会创建该 Bean。
java
package com.example.security.config;
import com.example.security.service.provider.EmailProvider;
import com.sendgrid.SendGrid;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SendGridConfig {
@Bean
@ConditionalOnProperty(name = "email.provider.api-key")
public SendGrid sendGrid(EmailProvider emailProvider) {
return new SendGrid(emailProvider.getApiKey());
}
}
API 实现类
实现 EmailService,使用 SendGrid 的 Mail 对象构建邮件,并通过 sendGrid.api(request) 发送 HTTP 请求。同样通过 @ConditionalOnProperty 控制启用条件。
java
package com.example.security.service.impl;
import com.example.security.service.EmailService;
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
@ConditionalOnProperty(name = "email.provider.api-key")
public class SendGridEmailServiceImpl implements EmailService {
private final SendGrid sendGrid;
public SendGridEmailServiceImpl(SendGrid sendGrid) {
this.sendGrid = sendGrid;
}
@Override
public void send(String to, String content) {
Email from = new Email("noreply@yourdomain.com");
Email recipient = new Email(to);
Content mailContent = new Content("text/plain", content);
Mail mail = new Mail(from, "Verification Code", recipient, mailContent);
Request request = new Request();
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
try {
request.setBody(mail.build());
Response response = sendGrid.api(request);
if (response.getStatusCode() == 202) {
// 发送成功,可记录日志或触发后续逻辑
System.out.println("Email sent successfully via SendGrid API");
} else {
// 处理非 202 状态
System.err.println("Failed to send email, status code: " + response.getStatusCode());
}
} catch (IOException ex) {
// 实际项目中应使用日志框架记录
System.err.println("SendGrid API call failed: " + ex.getMessage());
}
}
}
两种方式的对比
| 特性 | SMTP 方式 | API 方式(SendGrid) |
|---|---|---|
| 依赖 | spring-boot-starter-mail |
sendgrid-java |
| 配置复杂度 | 较低,只需 YAML 配置 | 需要额外配置类与 SDK |
| 发送速度 | 较慢,完整的 SMTP 握手过程 | 较快,直接 HTTP 调用 |
| 功能扩展 | 基本发送,无追踪能力 | 提供打开追踪、点击统计等高级功能 |
| 可用性 | 依赖自建或第三方 SMTP 服务器 | 依赖云服务商 SLA |
| 成本 | 一般与邮件服务器一起提供 | SendGrid 提供免费额度,超出后付费 |
与 Spring Security + OAuth2 的结合点
在典型的 Spring Security + OAuth2 授权服务器或资源服务器中,邮件发送常用于以下场景:
- 注册时邮箱验证 :用户提交注册信息后,生成一次性令牌,通过
EmailService发送到用户邮箱。 - 密码重置 :用户请求重置密码,系统生成带有效期的重置链接,调用
EmailService发送。 - 多因素认证(MFA):将邮件作为一种第二验证因子,发送一次性验证码。
- OAuth2 设备授权流程:某些设备授权需要向用户邮箱发送确认链接。
上述实现完全遵循开闭原则,你可以在 SmtpEmailServiceImpl 和 SendGridEmailServiceImpl 之间无缝切换,只需修改 application.yml 中的 email.provider.api-key 或 email.provider.smtp-enabled 即可。Spring Security 流程中只需注入 EmailService 接口,无需关心底层实现。
总结
本文从接口定义到两种主流邮件发送实现,完整展示了在 Spring Security + OAuth2 工程中引入邮件服务的方法。
所有代码均经过抽象与条件注解优化,可直接集成到你的项目中。通过环境变量与配置属性,实现了开发、测试、生产环境的灵活切换。
你可以根据实际需求选择 SMTP 直连或第三方 API,享受 Spring Boot 自动配置带来的便利。