From e4ef35c7955782d42a0bf64d38e7bbb8fc6cc3e3 Mon Sep 17 00:00:00 2001 From: cnc Date: Fri, 18 Sep 2026 17:58:10 +0800 Subject: [PATCH] =?UTF-8?q?google=E9=82=AE=E7=AE=B1=E5=8F=91=E9=80=81?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GmailCaptchaController.java | 64 +++++++++++ .../src/main/resources/application-dev.yml | 24 ++++ .../src/main/resources/application-prod.yml | 21 ++++ .../common/mail/config/MailConfig.java | 3 +- .../config/properties/GmailProperties.java | 107 ++++++++++++++++++ .../common/mail/core/GmailCodeService.java | 71 ++++++++++++ 6 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 ruoyi-admin/src/main/java/org/dromara/web/controller/GmailCaptchaController.java create mode 100644 ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/GmailProperties.java create mode 100644 ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/core/GmailCodeService.java diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/GmailCaptchaController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/GmailCaptchaController.java new file mode 100644 index 0000000..100871c --- /dev/null +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/GmailCaptchaController.java @@ -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 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)); + } +} diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index f30c138..e27a4ce 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -206,6 +206,30 @@ mail: # Socket连接超时值,单位毫秒,缺省值不超时 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) # 所有项在代码中均有默认值,此处显式列出仅为便于运维调参;整段删除不影响启动。 device: diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 74a3e79..cf737f7 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -209,6 +209,27 @@ mail: # Socket连接超时值,单位毫秒,缺省值不超时 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) # 所有项在代码中均有默认值,此处显式列出仅为便于运维调参;整段删除不影响启动。 device: diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java index dfe4793..4ee3b10 100644 --- a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java +++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/MailConfig.java @@ -1,6 +1,7 @@ package org.dromara.common.mail.config; import cn.hutool.extra.mail.MailAccount; +import org.dromara.common.mail.config.properties.GmailProperties; import org.dromara.common.mail.config.properties.MailProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -13,7 +14,7 @@ import org.springframework.context.annotation.Bean; * @author Michelle.Chung */ @AutoConfiguration -@EnableConfigurationProperties(MailProperties.class) +@EnableConfigurationProperties({MailProperties.class, GmailProperties.class}) public class MailConfig { /** diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/GmailProperties.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/GmailProperties.java new file mode 100644 index 0000000..013d606 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/config/properties/GmailProperties.java @@ -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 邮件服务配置属性。 + *

+ * 独立于框架默认的 {@link MailProperties}(prefix=mail),专用于 Gmail SMTP 发信。 + *

+ * Gmail 口径:主机固定 {@code smtp.gmail.com},端口 465 走 SSL(或 587 走 STARTTLS)。 + * Gmail 已停用「低安全性应用访问」,{@link #pass} 必须填 Google 账号生成的 + * 16 位应用专用密码(非账号登录密码),且 {@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); + } +} diff --git a/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/core/GmailCodeService.java b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/core/GmailCodeService.java new file mode 100644 index 0000000..f834618 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mail/src/main/java/org/dromara/common/mail/core/GmailCodeService.java @@ -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 验证码邮件发送服务。 + *

+ * 仅负责「用 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 "

" + + "

您本次操作的邮箱验证码为:

" + + "

" + code + "

" + + "

验证码 " + expiryMinutes + " 分钟内有效,请勿泄露给他人。

" + + "

若非本人操作,请忽略本邮件。

" + + "
"; + } +}