Java框架快速入门: Spring Security+OAuth2之邮件验证码发送(SMTP与API方式)

概述

在实现用户认证、密码重置等场景时,邮件发送是不可或缺的一环。本文将基于 Spring Boot 生态,分别使用 SMTP 协议第三方 API(以 SendGrid 为例) 两种方式完成邮件发送服务,并与 Spring Security + OAuth2 体系无缝集成。

你将获得一套可直接复制运行的工程代码,涵盖接口定义、条件化配置、发送逻辑等完整链路。

纲要

  • 核心流程概览
  • 项目结构一览
  • 邮件服务接口定义
  • SMTP 方式实现
    • 添加 spring-boot-starter-mail 依赖
    • application.yml SMTP 配置
    • 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 设备授权流程:某些设备授权需要向用户邮箱发送确认链接。

上述实现完全遵循开闭原则,你可以在 SmtpEmailServiceImplSendGridEmailServiceImpl 之间无缝切换,只需修改 application.yml 中的 email.provider.api-keyemail.provider.smtp-enabled 即可。Spring Security 流程中只需注入 EmailService 接口,无需关心底层实现。

总结

本文从接口定义到两种主流邮件发送实现,完整展示了在 Spring Security + OAuth2 工程中引入邮件服务的方法。

所有代码均经过抽象与条件注解优化,可直接集成到你的项目中。通过环境变量与配置属性,实现了开发、测试、生产环境的灵活切换。

你可以根据实际需求选择 SMTP 直连或第三方 API,享受 Spring Boot 自动配置带来的便利。

相关推荐
砚底藏山河2 小时前
容错重试与指数退避:网络抖动手抖不再丢数据(魔码量化实战 #04)
java·数据库·python·金融
2501_930472442 小时前
云安全自查清单:用腾讯云助手 30 分钟完成一次权限与密钥体检
java·腾讯云
蓝速科技2 小时前
会议室门牌触控预约选型与落地实战指南丨蓝速科技
大数据·运维·数据库·人工智能·科技
蓝速科技3 小时前
企业展厅数字人导览效果提升与选型实战指南丨蓝速科技
大数据·运维·数据库·人工智能·科技·microsoft
奔跑吧树袋熊3 小时前
多租户隔离该做到哪一层:从连接池到 ORM 的取舍
开发语言·数据库·oracle·系统架构
letisgo53 小时前
JAVA 高级进阶05篇《Spring容器机制:Bean生命周期与三级缓存全解》
java·spring·面试·ioc·源码分析
岁月如歌77863 小时前
分布式锁完全指南:从数据库到 Redisson 的演进
java·后端·架构
AI深栈3 小时前
第 8 章 · Tool Calling 与 Tool Search
java·人工智能
罗光记3 小时前
全能 Go Agent 框架 Covonaut v1.1.2发布:终端对话加入旁问与三档输出
数据库·经验分享·其他·百度·新浪微博