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 String SMS_CODE_KEY = "sms";
|
||||||
// 验证码过期时间
|
// 验证码过期时间
|
||||||
public static final Long SMS_CODE_TTL = 5L;
|
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
|
// 微信凭证access-token
|
||||||
public static final String ACCESS_TOKEN_KEY = "access-token";
|
public static final String ACCESS_TOKEN_KEY = "access-token";
|
||||||
// 空值防止击穿数据库
|
// 空值防止击穿数据库
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
"/webjars/**",
|
"/webjars/**",
|
||||||
"/hxz/v1/**",
|
"/hxz/v1/**",
|
||||||
"/api/sendSmsCaptcha",
|
"/api/sendSmsCaptcha",
|
||||||
|
"/api/sendEmailCaptcha",
|
||||||
"/api/loginBySms",
|
"/api/loginBySms",
|
||||||
"/api/loginBySuperAdminSms",
|
"/api/loginBySuperAdminSms",
|
||||||
"/api/loginByDeveloperSms",
|
"/api/loginByDeveloperSms",
|
||||||
|
|||||||
@@ -290,4 +290,20 @@ public class CommonUtil {
|
|||||||
return pattern.matcher(phoneNumber).matches();
|
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.mapper.CompanyMapper;
|
||||||
import com.gxwebsoft.common.system.param.LoginParam;
|
import com.gxwebsoft.common.system.param.LoginParam;
|
||||||
import com.gxwebsoft.common.system.param.SmsCaptchaParam;
|
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.FindAccountByPhoneParam;
|
||||||
import com.gxwebsoft.common.system.param.ResetPasswordParam;
|
import com.gxwebsoft.common.system.param.ResetPasswordParam;
|
||||||
import com.gxwebsoft.common.system.param.UpdatePasswordParam;
|
import com.gxwebsoft.common.system.param.UpdatePasswordParam;
|
||||||
@@ -390,8 +392,33 @@ public class MainController extends BaseController {
|
|||||||
update.setAvatar(user.getAvatar());
|
update.setAvatar(user.getAvatar());
|
||||||
update.setBgImage(user.getBgImage());
|
update.setBgImage(user.getBgImage());
|
||||||
update.setSex(user.getSex());
|
update.setSex(user.getSex());
|
||||||
update.setPhone(user.getPhone());
|
// 手机号变更:仅当提交短信验证码且校验通过时才更新,防止未经验证的手机号被绑定
|
||||||
|
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());
|
update.setEmail(user.getEmail());
|
||||||
|
redisUtil.delete(key);
|
||||||
|
}
|
||||||
|
// 未提交邮箱验证码时不更新邮箱(保持原值,禁止绕过校验)
|
||||||
update.setProvince(user.getProvince());
|
update.setProvince(user.getProvince());
|
||||||
update.setCity(user.getCity());
|
update.setCity(user.getCity());
|
||||||
update.setRegion(user.getRegion());
|
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
|
@OperationLog
|
||||||
@Operation(summary = "重置密码")
|
@Operation(summary = "重置密码")
|
||||||
@PutMapping("/password")
|
@PutMapping("/password")
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ public class User implements UserDetails {
|
|||||||
@Schema(description = "邮箱")
|
@Schema(description = "邮箱")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String emailCode;
|
||||||
|
|
||||||
|
@Schema(description = "短信验证码(非数据库字段,仅用于绑定/修改手机号时校验)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String smsCode;
|
||||||
|
|
||||||
@Schema(description = "资质")
|
@Schema(description = "资质")
|
||||||
private String aptitude;
|
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);
|
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