feat(auth): 实现邮箱验证码发送与校验功能
- 新增邮箱格式校验方法 isValidEmail - 添加发送邮箱验证码的邮件模板方法 sendCaptchaEmail - 在用户信息更新时添加邮箱验证码校验逻辑,确保邮箱变更安全 - 新增 API 接口 /sendEmailCaptcha 用于发送邮箱验证码 - 添加 EmailCaptchaParam 参数类支持邮箱验证码请求数据 - 增加邮箱验证码的 Redis 缓存及过期时间常量定义 - 更新安全配置,允许 /api/sendEmailCaptcha 免认证访问 - User实体新增 emailCode 和 smsCode 字段用于验证码校验
This commit is contained in:
@@ -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";
|
||||
// 空值防止击穿数据库
|
||||
|
||||
@@ -54,6 +54,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
"/webjars/**",
|
||||
"/hxz/v1/**",
|
||||
"/api/sendSmsCaptcha",
|
||||
"/api/sendEmailCaptcha",
|
||||
"/api/loginBySms",
|
||||
"/api/loginBySuperAdminSms",
|
||||
"/api/loginByDeveloperSms",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user