google邮箱发送验证码
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
package org.dromara.web.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
|
import org.dromara.common.core.domain.R;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.common.core.utils.regex.RegexValidator;
|
||||||
|
import org.dromara.common.mail.core.GmailCodeService;
|
||||||
|
import org.dromara.common.redis.annotation.RateLimiter;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gmail 邮箱验证码接口。
|
||||||
|
*
|
||||||
|
* @author Qoder
|
||||||
|
*/
|
||||||
|
@SaIgnore
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
public class GmailCaptchaController {
|
||||||
|
|
||||||
|
private final GmailCodeService gmailCodeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Gmail 发送邮箱验证码。
|
||||||
|
*
|
||||||
|
* @param email 收件邮箱
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/resource/gmail/code")
|
||||||
|
public R<Void> gmailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
||||||
|
if (!RegexValidator.isEmail(email)) {
|
||||||
|
return R.fail("请输入正确的邮箱地址!");
|
||||||
|
}
|
||||||
|
SpringUtils.getAopProxy(this).gmailCodeImpl(email);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码的实际执行方法,拆分出来以便限流注解生效。
|
||||||
|
*
|
||||||
|
* @param email 收件邮箱
|
||||||
|
*/
|
||||||
|
@RateLimiter(key = "#email", time = 60, count = 1)
|
||||||
|
public void gmailCodeImpl(String email) {
|
||||||
|
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
|
||||||
|
String code = RandomUtil.randomNumbers(4);
|
||||||
|
// 先落缓存再发信:即使发信异常,验证码也不会丢失;发信失败会抛 ServiceException 由全局异常处理
|
||||||
|
gmailCodeService.sendVerifyCode(email, code, Constants.CAPTCHA_EXPIRATION);
|
||||||
|
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -206,6 +206,30 @@ mail:
|
|||||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||||
connectionTimeout: 0
|
connectionTimeout: 0
|
||||||
|
|
||||||
|
--- # gmail 邮件发送(Gmail SMTP,用于邮箱验证码)
|
||||||
|
gmail:
|
||||||
|
# 是否启用;未启用时接口返回「未开启 Gmail 邮件功能」
|
||||||
|
enabled: true
|
||||||
|
host: smtp.gmail.com
|
||||||
|
# 465 走 SSL;若改用 587,请同时设 sslEnable=false 且 starttlsEnable=true
|
||||||
|
port: 465
|
||||||
|
auth: true
|
||||||
|
# 发信账号(Gmail 地址),由环境变量注入
|
||||||
|
user: ${GMAIL_USER:huzuming2@gmail.com}
|
||||||
|
# user: ${GMAIL_USER:}
|
||||||
|
# user: "chinarzcui@gmail.com"
|
||||||
|
# 发信凭据:Gmail 16 位「应用专用密码」(非登录密码),由环境变量注入
|
||||||
|
pass: ${GMAIL_PASS:lajl mccm ycqi uekj}
|
||||||
|
# pass: ${GMAIL_PASS:}
|
||||||
|
# 发件人,留空回落到 user;Gmail 要求发件人与认证账号一致
|
||||||
|
from: ${GMAIL_FROM:huzuming2@gmail.com}
|
||||||
|
# from: ${GMAIL_FROM:}
|
||||||
|
sslEnable: true
|
||||||
|
starttlsEnable: false
|
||||||
|
# smtp.gmail.com 在国内机房通常不可达,快速失败避免挂死请求线程
|
||||||
|
connectionTimeout: 5000
|
||||||
|
timeout: 10000
|
||||||
|
|
||||||
--- # 设备域 · APP 端配置(前缀 device.app,对应 org.dromara.device.config.AppProperties)
|
--- # 设备域 · APP 端配置(前缀 device.app,对应 org.dromara.device.config.AppProperties)
|
||||||
# 所有项在代码中均有默认值,此处显式列出仅为便于运维调参;整段删除不影响启动。
|
# 所有项在代码中均有默认值,此处显式列出仅为便于运维调参;整段删除不影响启动。
|
||||||
device:
|
device:
|
||||||
|
|||||||
@@ -209,6 +209,27 @@ mail:
|
|||||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||||
connectionTimeout: 0
|
connectionTimeout: 0
|
||||||
|
|
||||||
|
--- # gmail 邮件发送(Gmail SMTP,用于邮箱验证码)
|
||||||
|
gmail:
|
||||||
|
# 是否启用;未启用时接口返回「未开启 Gmail 邮件功能」
|
||||||
|
enabled: true
|
||||||
|
host: smtp.gmail.com
|
||||||
|
# 465 走 SSL;若改用 587,请同时设 sslEnable=false 且 starttlsEnable=true
|
||||||
|
port: 465
|
||||||
|
auth: true
|
||||||
|
# 发信账号(Gmail 地址),由环境变量注入
|
||||||
|
user: ${GMAIL_USER:huzuming2@gmail.com}
|
||||||
|
# user: "chinarzcui@gmail.com"
|
||||||
|
# 发信凭据:Gmail 16 位「应用专用密码」(非登录密码),由环境变量注入
|
||||||
|
pass: ${GMAIL_PASS:qbjo lajl mccm ycqi uekj}
|
||||||
|
# 发件人,留空回落到 user;Gmail 要求发件人与认证账号一致
|
||||||
|
from: ${GMAIL_FROM:huzuming2@gmail.com}
|
||||||
|
sslEnable: true
|
||||||
|
starttlsEnable: false
|
||||||
|
# smtp.gmail.com 在国内机房通常不可达,快速失败避免挂死请求线程
|
||||||
|
connectionTimeout: 5000
|
||||||
|
timeout: 10000
|
||||||
|
|
||||||
--- # 设备域 · APP 端配置(前缀 device.app,对应 org.dromara.device.config.AppProperties)
|
--- # 设备域 · APP 端配置(前缀 device.app,对应 org.dromara.device.config.AppProperties)
|
||||||
# 所有项在代码中均有默认值,此处显式列出仅为便于运维调参;整段删除不影响启动。
|
# 所有项在代码中均有默认值,此处显式列出仅为便于运维调参;整段删除不影响启动。
|
||||||
device:
|
device:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.dromara.common.mail.config;
|
package org.dromara.common.mail.config;
|
||||||
|
|
||||||
import cn.hutool.extra.mail.MailAccount;
|
import cn.hutool.extra.mail.MailAccount;
|
||||||
|
import org.dromara.common.mail.config.properties.GmailProperties;
|
||||||
import org.dromara.common.mail.config.properties.MailProperties;
|
import org.dromara.common.mail.config.properties.MailProperties;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@@ -13,7 +14,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
* @author Michelle.Chung
|
* @author Michelle.Chung
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@EnableConfigurationProperties(MailProperties.class)
|
@EnableConfigurationProperties({MailProperties.class, GmailProperties.class})
|
||||||
public class MailConfig {
|
public class MailConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package org.dromara.common.mail.config.properties;
|
||||||
|
|
||||||
|
import cn.hutool.extra.mail.MailAccount;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gmail 邮件服务配置属性。
|
||||||
|
* <p>
|
||||||
|
* 独立于框架默认的 {@link MailProperties}(prefix=mail),专用于 Gmail SMTP 发信。
|
||||||
|
* <p>
|
||||||
|
* <b>Gmail 口径</b>:主机固定 {@code smtp.gmail.com},端口 465 走 SSL(或 587 走 STARTTLS)。
|
||||||
|
* Gmail 已停用「低安全性应用访问」,{@link #pass} 必须填 Google 账号生成的
|
||||||
|
* <b>16 位应用专用密码</b>(非账号登录密码),且 {@link #from} 必须等于 {@link #user},否则会被拒收。
|
||||||
|
*
|
||||||
|
* @author Qoder
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ConfigurationProperties(prefix = "gmail")
|
||||||
|
public class GmailProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用 Gmail 发信功能。
|
||||||
|
*/
|
||||||
|
private Boolean enabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP 服务器域名,Gmail 固定为 smtp.gmail.com。
|
||||||
|
*/
|
||||||
|
private String host = "smtp.gmail.com";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP 服务端口:465(SSL)或 587(STARTTLS)。
|
||||||
|
*/
|
||||||
|
private Integer port = 465;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要用户名密码验证。
|
||||||
|
*/
|
||||||
|
private Boolean auth = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发信账号(Gmail 地址),建议由环境变量注入。
|
||||||
|
*/
|
||||||
|
private String user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发信凭据:Gmail 的 16 位应用专用密码,建议由环境变量注入。
|
||||||
|
*/
|
||||||
|
private String pass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发件人,留空则回落到 {@link #user}。
|
||||||
|
*/
|
||||||
|
private String from;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 SSL 安全连接(465 端口用 true)。
|
||||||
|
*/
|
||||||
|
private Boolean sslEnable = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 STARTTLS 升级连接(587 端口用 true,与 {@link #sslEnable} 互斥)。
|
||||||
|
*/
|
||||||
|
private Boolean starttlsEnable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP 读写超时时长,单位毫秒。
|
||||||
|
*/
|
||||||
|
private Long timeout = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket 连接超时值,单位毫秒。Gmail 国内不可达时快速失败,避免挂死请求线程。
|
||||||
|
*/
|
||||||
|
private Long connectionTimeout = 5000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 Hutool 邮件账户配置。
|
||||||
|
*
|
||||||
|
* @return 邮件账户配置
|
||||||
|
*/
|
||||||
|
public MailAccount toMailAccount() {
|
||||||
|
MailAccount account = new MailAccount();
|
||||||
|
account.setHost(host);
|
||||||
|
account.setPort(port);
|
||||||
|
account.setAuth(auth);
|
||||||
|
account.setUser(user);
|
||||||
|
account.setPass(pass);
|
||||||
|
account.setFrom(StringUtils.isBlank(from) ? user : from);
|
||||||
|
account.setSocketFactoryPort(port);
|
||||||
|
account.setStarttlsEnable(starttlsEnable);
|
||||||
|
account.setSslEnable(sslEnable);
|
||||||
|
account.setTimeout(timeout);
|
||||||
|
account.setConnectionTimeout(connectionTimeout);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否已具备真实发信条件(账号与凭据均已配置)。
|
||||||
|
*
|
||||||
|
* @return true 已配置;false 未配置
|
||||||
|
*/
|
||||||
|
public boolean isConfigured() {
|
||||||
|
return StringUtils.isNotBlank(user) && StringUtils.isNotBlank(pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.dromara.common.mail.core;
|
||||||
|
|
||||||
|
import cn.hutool.extra.mail.MailAccount;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.dromara.common.mail.config.properties.GmailProperties;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gmail 验证码邮件发送服务。
|
||||||
|
* <p>
|
||||||
|
* 仅负责「用 Gmail 账户把验证码邮件发出去」,验证码的生成、限流、缓存由调用方(Controller)承担,
|
||||||
|
* 保持本服务的单一职责,便于在各业务模块复用。
|
||||||
|
*
|
||||||
|
* @author Qoder
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GmailCodeService {
|
||||||
|
|
||||||
|
private final GmailProperties gmailProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Gmail 发送邮箱验证码。
|
||||||
|
*
|
||||||
|
* @param to 收件邮箱
|
||||||
|
* @param code 验证码
|
||||||
|
* @param expiryMinutes 有效期(分钟),仅用于文案展示
|
||||||
|
*/
|
||||||
|
public void sendVerifyCode(String to, String code, int expiryMinutes) {
|
||||||
|
if (!Boolean.TRUE.equals(gmailProperties.getEnabled())) {
|
||||||
|
throw new ServiceException("当前系统未开启 Gmail 邮件功能!");
|
||||||
|
}
|
||||||
|
if (!gmailProperties.isConfigured()) {
|
||||||
|
throw new ServiceException("Gmail 发信账号或应用专用密码未配置!");
|
||||||
|
}
|
||||||
|
MailAccount account = gmailProperties.toMailAccount();
|
||||||
|
try {
|
||||||
|
MailBuilder.of(account)
|
||||||
|
.to(to)
|
||||||
|
.subject("邮箱验证码")
|
||||||
|
.html(buildHtml(code, expiryMinutes))
|
||||||
|
.send();
|
||||||
|
log.info("[Gmail] 验证码邮件已发送:收件邮箱={}", to);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 不记录验证码本身,避免真实发信失败时把验证码写进日志
|
||||||
|
log.error("[Gmail] 验证码邮件发送异常 to={} host={}:{}(若为连接超时,请确认服务器可访问 smtp.gmail.com)",
|
||||||
|
to, gmailProperties.getHost(), gmailProperties.getPort(), e);
|
||||||
|
throw new ServiceException("验证码邮件发送失败,请稍后重试!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造验证码邮件正文(纯 HTML,无外部资源,避免被邮件客户端拦截)。
|
||||||
|
*
|
||||||
|
* @param code 验证码
|
||||||
|
* @param expiryMinutes 有效期(分钟)
|
||||||
|
* @return HTML 正文
|
||||||
|
*/
|
||||||
|
private String buildHtml(String code, int expiryMinutes) {
|
||||||
|
return "<div style=\"font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"
|
||||||
|
+ "font-size:14px;line-height:1.6;color:#2c2c2a\">"
|
||||||
|
+ "<p>您本次操作的邮箱验证码为:</p>"
|
||||||
|
+ "<p style=\"font-size:24px;font-weight:500;letter-spacing:4px;margin:16px 0\">" + code + "</p>"
|
||||||
|
+ "<p>验证码 " + expiryMinutes + " 分钟内有效,请勿泄露给他人。</p>"
|
||||||
|
+ "<p style=\"color:#5f5e5a\">若非本人操作,请忽略本邮件。</p>"
|
||||||
|
+ "</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user