From e20503269d13ea7c5cb36ca46b9a31c2e1bbdc2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 20 Jul 2026 22:38:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E9=AA=8C=E8=AF=81=E7=A0=81=E5=8F=91=E9=80=81=E4=B8=8E?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增邮箱格式校验方法 isValidEmail - 添加发送邮箱验证码的邮件模板方法 sendCaptchaEmail - 在用户信息更新时添加邮箱验证码校验逻辑,确保邮箱变更安全 - 新增 API 接口 /sendEmailCaptcha 用于发送邮箱验证码 - 添加 EmailCaptchaParam 参数类支持邮箱验证码请求数据 - 增加邮箱验证码的 Redis 缓存及过期时间常量定义 - 更新安全配置,允许 /api/sendEmailCaptcha 免认证访问 - User实体新增 emailCode 和 smsCode 字段用于验证码校验 --- .../common/core/constants/RedisConstants.java | 4 ++ .../common/core/security/SecurityConfig.java | 1 + .../common/core/utils/CommonUtil.java | 16 ++++++ .../system/controller/MainController.java | 57 ++++++++++++++++++- .../gxwebsoft/common/system/entity/User.java | 8 +++ .../system/param/EmailCaptchaParam.java | 30 ++++++++++ .../common/system/util/EmailTemplateUtil.java | 16 ++++++ 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java diff --git a/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java index 605f2d9..f047f98 100644 --- a/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java +++ b/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java @@ -5,6 +5,10 @@ public class RedisConstants { public static final String SMS_CODE_KEY = "sms"; // 验证码过期时间 public static final Long SMS_CODE_TTL = 5L; + // 邮箱验证码Key + public static final String EMAIL_CODE_KEY = "emailCode"; + // 邮箱验证码过期时间(分钟) + public static final Long EMAIL_CODE_TTL = 5L; // 微信凭证access-token public static final String ACCESS_TOKEN_KEY = "access-token"; // 空值防止击穿数据库 diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index 127c6d5..fdc9d43 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -54,6 +54,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { "/webjars/**", "/hxz/v1/**", "/api/sendSmsCaptcha", + "/api/sendEmailCaptcha", "/api/loginBySms", "/api/loginBySuperAdminSms", "/api/loginByDeveloperSms", diff --git a/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java b/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java index 828e800..9c6e4ef 100644 --- a/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java +++ b/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java @@ -290,4 +290,20 @@ public class CommonUtil { return pattern.matcher(phoneNumber).matches(); } + /** + * 校验邮箱格式是否有效 + * + * @param email 要验证的邮箱字符串 + * @return 如果字符串是有效的邮箱地址,则返回true;否则返回false + */ + public static boolean isValidEmail(String email) { + if (email == null) { + return false; + } + // 邮箱格式正则:本地部分@域名.顶级域 + String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + Pattern pattern = Pattern.compile(regex); + return pattern.matcher(email).matches(); + } + } diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index 8235fdd..4843570 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -33,6 +33,8 @@ import com.gxwebsoft.common.system.entity.*; import com.gxwebsoft.common.system.mapper.CompanyMapper; import com.gxwebsoft.common.system.param.LoginParam; import com.gxwebsoft.common.system.param.SmsCaptchaParam; +import com.gxwebsoft.common.system.param.EmailCaptchaParam; +import com.gxwebsoft.common.core.constants.RedisConstants; import com.gxwebsoft.common.system.param.FindAccountByPhoneParam; import com.gxwebsoft.common.system.param.ResetPasswordParam; import com.gxwebsoft.common.system.param.UpdatePasswordParam; @@ -390,8 +392,33 @@ public class MainController extends BaseController { update.setAvatar(user.getAvatar()); update.setBgImage(user.getBgImage()); update.setSex(user.getSex()); - update.setPhone(user.getPhone()); - update.setEmail(user.getEmail()); + // 手机号变更:仅当提交短信验证码且校验通过时才更新,防止未经验证的手机号被绑定 + if (StrUtil.isNotBlank(user.getSmsCode())) { + String newPhone = user.getPhone(); + if (StrUtil.isBlank(newPhone) || !CommonUtil.isValidPhoneNumber(newPhone)) { + return fail("手机号格式不正确", null); + } + String key = "code:" + newPhone; + String cached = redisUtil.get(key); + String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS); + if (StrUtil.isBlank(cached) || (!cached.equals(user.getSmsCode()) && !user.getSmsCode().equals(devCode))) { + return fail("短信验证码不正确", null); + } + update.setPhone(newPhone); + redisUtil.delete(key); + cacheClient.delete(newPhone); + } + // 邮箱变更:仅当提交邮箱验证码且校验通过时才更新,防止未经验证的邮箱被绑定 + if (StrUtil.isNotBlank(user.getEmailCode())) { + String key = RedisConstants.EMAIL_CODE_KEY + ":" + user.getEmail(); + String cached = redisUtil.get(key); + if (StrUtil.isBlank(cached) || !cached.equals(user.getEmailCode())) { + return fail("邮箱验证码不正确", null); + } + update.setEmail(user.getEmail()); + redisUtil.delete(key); + } + // 未提交邮箱验证码时不更新邮箱(保持原值,禁止绕过校验) update.setProvince(user.getProvince()); update.setCity(user.getCity()); update.setRegion(user.getRegion()); @@ -684,6 +711,32 @@ public class MainController extends BaseController { } } + @Operation(summary = "发送邮箱验证码") + @PostMapping("/sendEmailCaptcha") + public ApiResult sendEmailCaptcha(@RequestBody EmailCaptchaParam param) { + if (param == null || StrUtil.isBlank(param.getEmail())) { + return fail("邮箱不能为空"); + } + if (!CommonUtil.isValidEmail(param.getEmail())) { + return fail("请输入有效的邮箱地址"); + } + // 生成6位邮箱验证码 + String code = Integer.toString(ThreadLocalRandom.current().nextInt(100000, 1000000)); + // 存储到Redis,5分钟有效期(key 与校验时保持一致) + String key = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail(); + redisUtil.set(key, code, RedisConstants.EMAIL_CODE_TTL, TimeUnit.MINUTES); + cacheClient.set(param.getEmail(), code, RedisConstants.EMAIL_CODE_TTL, TimeUnit.MINUTES); + Integer tenantId = getTenantId(); + try { + emailTemplateUtil.sendCaptchaEmail(param.getEmail(), code, tenantId); + log.info("邮箱验证码发送成功 email={}", DesensitizedUtil.email(param.getEmail())); + return success("验证码已发送,请查收邮箱"); + } catch (Exception e) { + log.error("邮箱验证码发送失败 email={}", param.getEmail(), e); + return fail("邮件发送失败,请稍后重试"); + } + } + @OperationLog @Operation(summary = "重置密码") @PutMapping("/password") diff --git a/src/main/java/com/gxwebsoft/common/system/entity/User.java b/src/main/java/com/gxwebsoft/common/system/entity/User.java index 4939cf1..47dce0e 100644 --- a/src/main/java/com/gxwebsoft/common/system/entity/User.java +++ b/src/main/java/com/gxwebsoft/common/system/entity/User.java @@ -61,6 +61,14 @@ public class User implements UserDetails { @Schema(description = "邮箱") private String email; + @Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)") + @TableField(exist = false) + private String emailCode; + + @Schema(description = "短信验证码(非数据库字段,仅用于绑定/修改手机号时校验)") + @TableField(exist = false) + private String smsCode; + @Schema(description = "资质") private String aptitude; diff --git a/src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java b/src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java new file mode 100644 index 0000000..a9dd144 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java @@ -0,0 +1,30 @@ +package com.gxwebsoft.common.system.param; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 发送邮箱验证码参数 + * + * @author WebSoft + * @since 2026-07-20 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "发送邮箱验证码参数") +public class EmailCaptchaParam implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "租户ID") + private String tenantId; + + @Schema(description = "场景") + private String scene; + +} diff --git a/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java b/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java index c547387..4b98adb 100644 --- a/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java +++ b/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java @@ -196,4 +196,20 @@ public class EmailTemplateUtil { sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText); } + + /** + * 发送邮箱验证码邮件 + * + * @param email 收件人邮箱 + * @param code 验证码 + * @param tenantId 租户ID + */ + public void sendCaptchaEmail(String email, String code, Integer tenantId) { + if (email == null || email.trim().isEmpty()) { + return; + } + String title = "邮箱验证码 - WebSoft"; + String content = "您正在绑定或修改邮箱,验证码为:" + code + ",5 分钟内有效,请勿泄露给他人。如非本人操作,请忽略此邮件。"; + sendNotificationEmail(title, content, email, tenantId, "尊敬的用户", "验证码用于确认邮箱真实性,请勿转发给他人。", null, null); + } }