增加忘记密码

This commit is contained in:
wwb 2024-12-08 20:45:23 +08:00
parent 47b6f6a8a5
commit 5ad0473e03
30 changed files with 780 additions and 51 deletions

View File

@ -24,11 +24,20 @@ public class ValidationUtils {
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
private static final Pattern PATTERN_EMAIL = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
public static boolean isMobile(String mobile) {
return StringUtils.hasText(mobile)
&& PATTERN_MOBILE.matcher(mobile).matches();
}
public static boolean isEmail(String email) {
return StringUtils.hasText(email)
&& PATTERN_EMAIL.matcher(email).matches();
}
public static boolean isURL(String url) {
return StringUtils.hasText(url)
&& PATTERN_URL.matcher(url).matches();

View File

@ -0,0 +1,28 @@
package cn.hangtag.framework.common.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = EmailValidator.class
)
public @interface Email {
String message() default "邮箱格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,25 @@
package cn.hangtag.framework.common.validation;
import cn.hangtag.framework.common.util.validation.ValidationUtils;
import cn.hutool.core.util.StrUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmailValidator implements ConstraintValidator<Email, String> {
@Override
public void initialize(Email annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手机号为空默认不校验即校验通过
if (StrUtil.isEmpty(value)) {
return true;
}
// 校验手机
return ValidationUtils.isEmail(value);
}
}

View File

@ -0,0 +1,43 @@
package cn.hangtag.module.system.api.mail;
import cn.hangtag.framework.common.exception.ServiceException;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import javax.validation.Valid;
/**
* 短信验证码 API 接口
*
* @author 芋道源码
*/
public interface MailCodeApi {
/**
* 创建邮箱验证码并进行发送
*
* @param reqDTO 发送请求
*/
void sendMailCode(@Valid MailCodeSendReqDTO reqDTO);
/**
* 验证邮箱验证码并进行使用
* 如果正确则将验证码标记成已使用
* 如果错误则抛出 {@link ServiceException} 异常
*
* @param reqDTO 使用请求
*/
void useMailCode(@Valid MailCodeUseReqDTO reqDTO);
/**
* 检查验证码是否有效
*
* @param reqDTO 校验请求
*/
void validateMailCode(@Valid MailCodeValidateReqDTO reqDTO);
}

View File

@ -1,7 +1,7 @@
package cn.hangtag.module.system.api.mail.dto.code;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.framework.common.validation.Mobile;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import lombok.Data;
@ -19,7 +19,7 @@ public class MailCodeSendReqDTO {
/**
* 邮箱
*/
@Mobile
@Email
@NotEmpty(message = "邮箱不能为空")
private String mail;
/**

View File

@ -1,5 +1,6 @@
package cn.hangtag.module.system.api.mail.dto.code;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.framework.common.validation.Mobile;
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
@ -19,7 +20,7 @@ public class MailCodeUseReqDTO {
/**
* 邮箱
*/
@Mobile
@Email
@NotEmpty(message = "邮箱不能为空")
private String mail;
/**

View File

@ -8,7 +8,7 @@ import lombok.Getter;
import java.util.Arrays;
/**
* 用户短信验证码发送场景的枚举
* 用户邮箱验证码发送场景的枚举
*
* @author 芋道源码
*/
@ -16,12 +16,12 @@ import java.util.Arrays;
@AllArgsConstructor
public enum MailSceneEnum implements IntArrayValuable {
MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 邮箱登陆"),
MEMBER_UPDATE_MOBILE(2, "user-update-mobile", "会员用户 - 修改手机"),
MEMBER_LOGIN(1, "user-mail-login", "会员用户 - 邮箱登陆"),
MEMBER_UPDATE_MOBILE(2, "user-update-mail", "会员用户 - 修改手机"),
MEMBER_UPDATE_PASSWORD(3, "user-update-password", "会员用户 - 修改密码"),
MEMBER_RESET_PASSWORD(4, "user-reset-password", "会员用户 - 忘记密码"),
ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 邮箱登录");
ADMIN_MEMBER_LOGIN(21, "admin-mail-login", "后台用户 - 邮箱登录");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MailSceneEnum::getScene).toArray();

View File

@ -0,0 +1,44 @@
package cn.hangtag.module.system.api.mail;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.api.sms.SmsCodeApi;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import cn.hangtag.module.system.service.mail.MailCodeService;
import cn.hangtag.module.system.service.sms.SmsCodeService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 短信验证码 API 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class MailCodeApiImpl implements MailCodeApi {
@Resource
private MailCodeService mailCodeService;
@Override
public void sendMailCode(MailCodeSendReqDTO reqDTO) {
mailCodeService.sendMailCode(reqDTO);
}
@Override
public void useMailCode(MailCodeUseReqDTO reqDTO) {
mailCodeService.useMailCode(reqDTO);
}
@Override
public void validateMailCode(MailCodeValidateReqDTO reqDTO) {
mailCodeService.validateMailCode(reqDTO);
}
}

View File

@ -146,6 +146,27 @@ public class AuthController {
return success(true);
}
// ========== 邮箱相关 ==========
@PostMapping("/send-mail-code")
@PermitAll
@Operation(summary = "发送邮箱验证码")
public CommonResult<Boolean> sendForgetPasswordEmailCode(@RequestBody @Valid AuthMailSendReqVO reqVO) {
authService.sendMailCode(reqVO);
return success(true);
}
@PostMapping("/mail-modifypwd")
@PermitAll
@Operation(summary = "使用邮箱验证码修改密码")
public CommonResult<Boolean> smsLogin(@RequestBody @Valid AuthMailModifyPwdReqVO reqVO) {
authService.mailResetPwd(reqVO);
return success(true);
}
// ========== 社交登录相关 ==========
@GetMapping("/social-auth-redirect")

View File

@ -23,8 +23,8 @@ public class AuthLoginReqVO {
@Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "hangtagyuanma")
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
//@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
//@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")

View File

@ -0,0 +1,37 @@
package cn.hangtag.module.system.controller.admin.auth.vo;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 邮箱验证码的重置密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthMailModifyPwdReqVO {
@Schema(description = "邮箱账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "邮箱账号不能为空")
@Email
private String mail;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "密码不能为空")
private String password;
@Schema(description = "确认密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "确认密码不能为空")
private String checkpassword;
@Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "验证码不能为空")
private String code;
}

View File

@ -0,0 +1,33 @@
package cn.hangtag.module.system.controller.admin.auth.vo;
import cn.hangtag.framework.common.validation.Email;
import cn.hangtag.framework.common.validation.InEnum;
import cn.hangtag.framework.common.validation.Mobile;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 发送手机验证码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthMailSendReqVO {
@Schema(description = "邮箱账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxxx@qq.com")
@NotEmpty(message = "邮箱账号不能为空")
@Email
private String mail;
@Schema(description = "邮箱场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "发送场景不能为空")
@InEnum(MailSceneEnum.class)
private Integer scene;
}

View File

@ -1,5 +1,7 @@
package cn.hangtag.module.system.convert.auth;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hutool.core.collection.CollUtil;
import cn.hangtag.framework.common.util.object.BeanUtils;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
@ -85,4 +87,8 @@ public interface AuthConvert {
SmsCodeUseReqDTO convert(AuthSmsLoginReqVO reqVO, Integer scene, String usedIp);
MailCodeSendReqDTO convert(AuthMailSendReqVO reqVO);
MailCodeUseReqDTO convert(AuthMailModifyPwdReqVO reqVO, Integer scene, String usedIp);
}

View File

@ -0,0 +1,65 @@
package cn.hangtag.module.system.dal.dataobject.mail;
import cn.hangtag.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 邮箱验证码 DO
*
* idx_mobile 索引基于 {@link #mobile} 字段
*
* @author 芋道源码
*/
@TableName("system_mail_code")
@KeySequence("system_mail_code_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MailCodeDO extends BaseDO {
/**
* 编号
*/
private Long id;
/**
* 邮箱号
*/
private String mail;
/**
* 验证码
*/
private String code;
/**
* 发送场景
*
* 枚举 {@link MailCodeDO}
*/
private Integer scene;
/**
* 创建 IP
*/
private String createIp;
/**
* 今日发送的第几条
*/
private Integer todayIndex;
/**
* 是否使用
*/
private Boolean used;
/**
* 使用时间
*/
private LocalDateTime usedTime;
/**
* 使用 IP
*/
private String usedIp;
}

View File

@ -0,0 +1,29 @@
package cn.hangtag.module.system.dal.mysql.mail;
import cn.hangtag.framework.mybatis.core.mapper.BaseMapperX;
import cn.hangtag.framework.mybatis.core.query.QueryWrapperX;
import cn.hangtag.module.system.dal.dataobject.mail.MailCodeDO;
import cn.hangtag.module.system.dal.dataobject.sms.SmsCodeDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MailCodeMapper extends BaseMapperX<MailCodeDO> {
/**
* 获得手机号的最后一个手机验证码
*
* @param mail 手机号
* @param scene 发送场景选填
* @param code 验证码 选填
* @return 手机验证码
*/
default MailCodeDO selectLastByMail(String mail, String code, Integer scene) {
return selectOne(new QueryWrapperX<MailCodeDO>()
.eq("mail", mail)
.eqIfPresent("scene", scene)
.eqIfPresent("code", code)
.orderByDesc("id")
.limitN(1));
}
}

View File

@ -70,4 +70,14 @@ public interface AdminAuthService {
*/
AuthLoginRespVO refreshToken(String refreshToken);
/**
* 邮箱验证码发送
*
* @param reqVO 发送请求
*/
void sendMailCode(AuthMailSendReqVO reqVO);
boolean mailResetPwd(AuthMailModifyPwdReqVO reqVO);
}

View File

@ -1,5 +1,8 @@
package cn.hangtag.module.system.service.auth;
import cn.hangtag.module.system.api.mail.MailCodeApi;
import cn.hangtag.module.system.dal.mysql.user.AdminUserMapper;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import cn.hutool.core.util.ObjectUtil;
import cn.hangtag.framework.common.enums.CommonStatusEnum;
import cn.hangtag.framework.common.enums.UserTypeEnum;
@ -29,6 +32,7 @@ import com.xingyuv.captcha.model.vo.CaptchaVO;
import com.xingyuv.captcha.service.CaptchaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@ -64,6 +68,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private CaptchaService captchaService;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private MailCodeApi mailCodeApi;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AdminUserMapper userMapper;
/**
* 验证码的开关默认为 true
@ -247,4 +258,43 @@ public class AdminAuthServiceImpl implements AdminAuthService {
return UserTypeEnum.ADMIN;
}
@Override
public void sendMailCode(AuthMailSendReqVO reqVO) {
// 登录场景验证是否存在
if (userService.getUserByMail(reqVO.getMail()) == null) {
throw exception(MAIL_ACCOUNT_NOT_EXISTS);
}
// 发送验证码
mailCodeApi.sendMailCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
}
@Override
public boolean mailResetPwd(AuthMailModifyPwdReqVO reqVO) {
// 校验验证码
mailCodeApi.useMailCode(AuthConvert.INSTANCE.convert(reqVO, MailSceneEnum.MEMBER_RESET_PASSWORD.getScene(), getClientIP()));
// 获得用户信息
AdminUserDO user = userService.getUserByMail(reqVO.getMail());
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 执行更新
user.setPassword(encodePassword(reqVO.getPassword())); // 加密密码
int i = userMapper.updateById(user);
return true;
}
/**
* 对密码进行加密
*
* @param password 密码
* @return 加密后的密码
*/
private String encodePassword(String password) {
return passwordEncoder.encode(password);
}
}

View File

@ -2,6 +2,8 @@ package cn.hangtag.module.system.service.mail;
import cn.hangtag.framework.common.exception.ServiceException;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
@ -28,13 +30,13 @@ public interface MailCodeService {
*
* @param reqDTO 使用请求
*/
void useMailCode(@Valid SmsCodeUseReqDTO reqDTO);
void useMailCode(@Valid MailCodeUseReqDTO reqDTO);
/**
* 检查验证码是否有效
*
* @param reqDTO 校验请求
*/
void validateMailCode(@Valid SmsCodeValidateReqDTO reqDTO);
void validateMailCode(@Valid MailCodeValidateReqDTO reqDTO);
}

View File

@ -1,13 +1,14 @@
package cn.hangtag.module.system.service.mail;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeUseReqDTO;
import cn.hangtag.module.system.api.mail.dto.code.MailCodeValidateReqDTO;
import cn.hangtag.module.system.dal.dataobject.mail.MailCodeDO;
import cn.hangtag.module.system.dal.dataobject.sms.SmsCodeDO;
import cn.hangtag.module.system.dal.mysql.sms.SmsCodeMapper;
import cn.hangtag.module.system.dal.mysql.mail.MailCodeMapper;
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
import cn.hangtag.module.system.framework.sms.config.SmsCodeProperties;
import cn.hangtag.module.system.service.sms.SmsCodeService;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
@ -16,6 +17,7 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import static cn.hangtag.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.hangtag.framework.common.util.date.DateUtils.isToday;
@ -35,32 +37,39 @@ public class MailCodeServiceImpl implements MailCodeService {
private SmsCodeProperties smsCodeProperties;
@Resource
private SmsCodeMapper smsCodeMapper;
private MailCodeMapper mailCodeMapper;
@Resource
private MailSendService mailSendService;
@Override
public void sendMailCode(MailCodeSendReqDTO reqDTO) {
SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene());
MailSceneEnum sceneEnum = MailSceneEnum.getCodeByScene(reqDTO.getScene());
Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene());
// 创建验证码
String code = createMailCode(reqDTO.getMail(), reqDTO.getScene(), reqDTO.getCreateIp());
LinkedHashMap<String, Object> params = new LinkedHashMap<>();
params.put("key1",reqDTO.getMail());
params.put("key2",reqDTO.getMail());
params.put("key3",code);
String templateCode = sceneEnum.getTemplateCode();
// 发送验证码
mailSendService.sendSingleMail(reqDTO.getMail(), null, null,
sceneEnum.getTemplateCode(), MapUtil.of("code", code));
sceneEnum.getTemplateCode(), params);
}
private String createMailCode(String mobile, Integer scene, String ip) {
// 校验是否可以发送验证码不用筛选场景
SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null);
if (lastSmsCode != null) {
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
MailCodeDO lastMailCode = mailCodeMapper.selectLastByMail(mobile, null, null);
if (lastMailCode != null) {
if (LocalDateTimeUtil.between(lastMailCode.getCreateTime(), LocalDateTime.now()).toMillis()
< smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
throw exception(SMS_CODE_SEND_TOO_FAST);
}
if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天才能计算超过当天的上限
lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限
if (isToday(lastMailCode.getCreateTime()) && // 必须是今天才能计算超过当天的上限
lastMailCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限
throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
}
// TODO 芋艿提升每个 IP 每天可发送数量
@ -70,44 +79,44 @@ public class MailCodeServiceImpl implements MailCodeService {
// 创建验证码记录
String code = String.format("%0" + smsCodeProperties.getEndCode().toString().length() + "d",
randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1));
SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene)
.todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1)
MailCodeDO newSmsCode = MailCodeDO.builder().mail(mobile).code(code).scene(scene)
.todayIndex(lastMailCode != null && isToday(lastMailCode.getCreateTime()) ? lastMailCode.getTodayIndex() + 1 : 1)
.createIp(ip).used(false).build();
smsCodeMapper.insert(newSmsCode);
mailCodeMapper.insert(newSmsCode);
return code;
}
@Override
public void useMailCode(SmsCodeUseReqDTO reqDTO) {
public void useMailCode(MailCodeUseReqDTO reqDTO) {
// 检测验证码是否有效
SmsCodeDO lastSmsCode = validateMailCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
MailCodeDO lastMailCode = validateMailCode0(reqDTO.getMail(), reqDTO.getCode(), reqDTO.getScene());
// 使用验证码
smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId())
mailCodeMapper.updateById(MailCodeDO.builder().id(lastMailCode.getId())
.used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build());
}
@Override
public void validateMailCode(SmsCodeValidateReqDTO reqDTO) {
validateMailCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
public void validateMailCode(MailCodeValidateReqDTO reqDTO) {
validateMailCode0(reqDTO.getMail(), reqDTO.getCode(), reqDTO.getScene());
}
private SmsCodeDO validateMailCode0(String mobile, String code, Integer scene) {
private MailCodeDO validateMailCode0(String mobile, String code, Integer scene) {
// 校验验证码
SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene);
MailCodeDO lastMailCode = mailCodeMapper.selectLastByMail(mobile, code, scene);
// 若验证码不存在抛出异常
if (lastSmsCode == null) {
if (lastMailCode == null) {
throw exception(SMS_CODE_NOT_FOUND);
}
// 超过时间
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
if (LocalDateTimeUtil.between(lastMailCode.getCreateTime(), LocalDateTime.now()).toMillis()
>= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期
throw exception(SMS_CODE_EXPIRED);
}
// 判断验证码是否已被使用
if (Boolean.TRUE.equals(lastSmsCode.getUsed())) {
if (Boolean.TRUE.equals(lastMailCode.getUsed())) {
throw exception(SMS_CODE_USED);
}
return lastSmsCode;
return lastMailCode;
}
}

View File

@ -12,6 +12,7 @@ import cn.hangtag.module.system.dal.mysql.mail.MailTemplateMapper;
import cn.hangtag.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@ -41,7 +42,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
/**
* 正则表达式匹配 {} 中的变量
*/
private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}");
private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{\\{(.*?)}}");
@Resource
private MailTemplateMapper mailTemplateMapper;
@ -122,6 +123,12 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override
public String formatMailTemplateContent(String content, Map<String, Object> params) {
if(StringUtils.isNotBlank(content)){
/* content = content.replaceFirst("<p>", "").replace("</p>", "");
content = content.replaceAll("&lt;", "<").replaceAll("&gt;", ">");
content = content.replaceAll("&nbsp;", "");*/
content = content.replaceAll("\\{\\{", "{").replaceAll("}}","}");
}
return StrUtil.format(content, params);
}

View File

@ -201,4 +201,15 @@ public interface AdminUserService {
*/
boolean isPasswordMatch(String rawPassword, String encodedPassword);
/**
* 通过邮箱号获取用户
*
* @param mail 邮箱号
* @return 用户对象信息
*/
AdminUserDO getUserByMail(String mail);
}

View File

@ -496,6 +496,11 @@ public class AdminUserServiceImpl implements AdminUserService {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
@Override
public AdminUserDO getUserByMail(String mail) {
return userMapper.selectByEmail(mail);
}
/**
* 对密码进行加密
*

View File

@ -251,8 +251,8 @@ hangtag:
expire-times: 10m
send-frequency: 1m
send-maximum-quantity-per-day: 10
begin-code: 9999 # 这里配置 9999 的原因是,测试方便。
end-code: 9999 # 这里配置 9999 的原因是,测试方便。
begin-code: 000001 # 这里配置 9999 的原因是,测试方便。
end-code: 999999 # 这里配置 9999 的原因是,测试方便。
trade:
order:
app-id: 1 # 商户编号

View File

@ -12,6 +12,19 @@ export interface SmsLoginVO {
code: string
}
export interface MailCodeVO {
mail: string
scene: number
}
export interface MailModifyPwdVO {
mail: string
password: string
checkpassword: string
code: string
}
// 登录
export const login = (data: UserLoginVO) => {
return request.post({ url: '/system/auth/login', data })
@ -47,6 +60,16 @@ export const sendSmsCode = (data: SmsCodeVO) => {
return request.post({ url: '/system/auth/send-sms-code', data })
}
//获取邮箱登录验证码
export const sendMailCode = (data: MailCodeVO) => {
return request.post({ url: '/system/auth/send-mail-code', data })
}
// 邮箱码修改密码
export const mailModifyPwd = (data: MailModifyPwdVO) => {
return request.post({ url: '/system/auth/mail-modifypwd', data })
}
// 短信验证码登录
export const smsLogin = (data: SmsLoginVO) => {
return request.post({ url: '/system/auth/sms-login', data })

View File

@ -114,7 +114,7 @@ export default {
},
login: {
welcome: 'Welcome to the system',
message: 'Backstage management system',
message: '',
tenantname: 'TenantName',
username: 'Username',
password: 'Password',
@ -133,14 +133,17 @@ export default {
codePlaceholder: 'Please Enter Verification Code',
mobileTitle: 'Mobile sign in',
mobileNumber: 'Mobile Number',
mobileNumberPlaceholder: 'Plaease Enter Mobile Number',
mobileNumberPlaceholder: 'Plaease Enter Mobile Code',
emailNumberPlaceholder: 'Plaease Enter Email Code',
backLogin: 'back',
getSmsCode: 'Get SMS Code',
getMailCode: 'Get Mail Code',
btnMobile: 'Mobile sign in',
btnQRCode: 'QR code sign in',
qrcode: 'Scan the QR code to log in',
btnRegister: 'Sign up',
SmsSendMsg: 'code has been sent'
SmsSendMsg: 'code has been sent',
pwdResetSuccess: 'password reset success'
},
captcha: {
verification: 'Please complete security verification',

View File

@ -134,13 +134,16 @@ export default {
mobileTitle: '手机登录',
mobileNumber: '手机号码',
mobileNumberPlaceholder: '请输入手机号码',
emailNumberPlaceholder: '请输入邮箱号码',
backLogin: '返回',
getSmsCode: '获取验证码',
getMailCode: '获取验证码',
btnMobile: '手机登录',
btnQRCode: '二维码登录',
qrcode: '扫描二维码登录',
btnRegister: '注册',
SmsSendMsg: '验证码已发送'
SmsSendMsg: '验证码已发送',
pwdResetSuccess: '密码重置成功'
},
captcha: {
verification: '请完成安全验证',

View File

@ -29,7 +29,7 @@
</div>
<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
<!-- 右上角的主题语言选择 -->
<!-- <div
<div
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
>
<div class="flex items-center at-2xl:hidden at-xl:hidden">
@ -40,7 +40,7 @@
<ThemeSwitch />
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
</div>
</div>-->
</div>
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
@ -56,6 +56,8 @@
<RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 三方登录 -->
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 忘记密码 -->
<ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div>
</Transition>
</div>
@ -70,7 +72,7 @@ import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
import { LoginForm, MobileForm, ForgetPasswordForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
defineOptions({ name: 'Login' })

View File

@ -0,0 +1,262 @@
<template>
<el-form
v-show="getShow"
ref="formSmsLogin"
:model="loginData.loginForm"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
autocomplete="off"
>
<el-row style="margin-right: -10px; margin-left: -10px">
<!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
</el-form-item>
</el-col>-->
<!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="mailNumber">
<el-input
v-model="loginData.loginForm.mailNumber"
:placeholder="t('login.emailNumberPlaceholder')"
:prefix-icon="iconCellemail"
/>
</el-form-item>
</el-col>
<!-- 密码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input
type="password"
disableautocomplete
autocomplete="off"
show-password="true"
v-model="loginData.loginForm.password"
:placeholder="t('login.password')"
/>
</el-form-item>
</el-col>
<!-- 确认密码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="checkPassword">
<el-input
type="password"
autocomplete="off"
disableautocomplete
show-password="true"
v-model="loginData.loginForm.checkPassword"
:placeholder="t('login.checkPassword')"
/>
</el-form-item>
</el-col>
<!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24">
<el-input
v-model="loginData.loginForm.code"
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<!-- <el-button class="w-[100%]"> -->
<template #append>
<span
v-if="mailCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getMailCode"
>
{{ t('login.getMailCode') }}
</span>
<span v-if="mailCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
{{ mailCodeTimer }}秒后可重新获取
</span>
</template>
</el-input>
<!-- </el-button> -->
</el-col>
</el-row>
</el-form-item>
</el-col>
<!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.login')"
class="w-[100%]"
type="primary"
@click="modifyPwdIn()"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.backLogin')"
class="w-[100%]"
@click="handleBackLogin()"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import { setTenantId, setToken } from '@/utils/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { getTenantIdByName, sendMailCode, mailModifyPwd } from '@/api/login'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
import { ElLoading } from 'element-plus'
defineOptions({ name: 'ForgetPasswordForm' })
const { t } = useI18n()
const message = useMessage()
const permissionStore = usePermissionStore()
const { currentRoute, push } = useRouter()
const formSmsLogin = ref()
const loginLoading = ref(false)
const iconHouse = useIcon({ icon: 'ep:house' })
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
const iconCellemail= useIcon({ icon: 'ep:cellphone' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { validForm } = useFormValid(formSmsLogin)
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
const rules = {
tenantName: [required],
mobileNumber: [required],
code: [required]
}
const loginData = reactive({
codeImg: '',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
token: '',
loading: {
modifyPwdIn: false
},
loginForm: {
uuid: '',
tenantName: '芋道源码',
mobileNumber: '',
code: ''
}
})
const mailVO = reactive({
mailCode: {
mail: '',
scene: 4
},
loginMail: {
mail: '',
code: ''
}
})
const mailCodeTimer = ref(0)
const redirect = ref<string>('')
const getMailCode = async () => {
await getTenantId()
mailVO.mailCode.mail = loginData.loginForm.mailNumber
await sendMailCode(mailVO.mailCode).then(async () => {
message.success(t('login.SmsSendMsg'))
//
mailCodeTimer.value = 60
let msgTimer = setInterval(() => {
mailCodeTimer.value = mailCodeTimer.value - 1
if (mailCodeTimer.value <= 0) {
clearInterval(msgTimer)
}
}, 1000)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
// ID
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = await getTenantIdByName(loginData.loginForm.tenantName)
setTenantId(res)
}
}
//
const modifyPwdIn = async () => {
await getTenantId()
const data = await validForm()
if (!data) return
/* ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})*/
loginLoading.value = true
mailVO.loginMail.mail = loginData.loginForm.mailNumber
mailVO.loginMail.code = loginData.loginForm.code
mailVO.loginMail.password = loginData.loginForm.password
mailVO.loginMail.checkpassword = loginData.loginForm.checkPassword
await mailModifyPwd(mailVO.loginMail)
.then(async (res) => {
/* setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
push({ path: redirect.value || permissionStore.addRouters[0].path })*/
await message.alertSuccess(t('login.pwdResetSuccess'))
handleBackLogin()
})
.catch(() => {})
.finally(() => {
loginLoading.value = false
setTimeout(() => {
const loadingInstance = ElLoading.service()
loadingInstance.close()
}, 400)
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}
</style>

View File

@ -58,9 +58,9 @@
{{ t('login.remember') }}
</el-checkbox>
</el-col>
<!-- <el-col :offset="6" :span="12">
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
</el-col>-->
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">{{ t('login.forgetPassword') }}</el-link>
</el-col>
</el-row>
</el-form-item>
</el-col>

View File

@ -1,8 +1,9 @@
import LoginForm from './LoginForm.vue'
import MobileForm from './MobileForm.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
import LoginFormTitle from './LoginFormTitle.vue'
import RegisterForm from './RegisterForm.vue'
import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
export { LoginForm, MobileForm, ForgetPasswordForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }