add:登录页面和注册页面增加手机号码选项

This commit is contained in:
hrz
2025-05-18 17:38:20 +08:00
parent 91d2ab7d1d
commit fe88db2094
25 changed files with 701 additions and 217 deletions
@@ -125,4 +125,18 @@ public class RedisKeys {
return "sms:Validate:Code:" + phone;
}
/**
* 获取短信验证码最后发送时间的缓存key
*/
public static String getSMSLastSendTimeKey(String phone) {
return "sms:Validate:Code:" + phone + ":last_send_time";
}
/**
* 获取短信验证码今日发送次数的缓存key
*/
public static String getSMSTodayCountKey(String phone) {
return "sms:Validate:Code:" + phone + ":today_count";
}
}
@@ -2,7 +2,6 @@ package xiaozhi.common.validator;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
@@ -49,12 +48,20 @@ public class ValidatorUtils {
}
/**
* 手机号正则表达式
* 国际手机号正则表达式
* 要求必须带国际区号,格式:+[国家代码][手机号]
* 例如:
* - +8613800138000
* - +12345678900
* - +447123456789
*/
private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
private static final String INTERNATIONAL_PHONE_REGEX = "^\\+[1-9]\\d{0,3}[1-9]\\d{4,14}$";
/**
* 校验单参数是否是正确的手机号
* 校验手机号是否有效
* 要求必须带国际区号,格式:+[国家代码][手机号]
* 例如:+8613800138000
*
* @param phone 手机号
* @return boolean
*/
@@ -62,8 +69,9 @@ public class ValidatorUtils {
if (phone == null || phone.isEmpty()) {
return false;
}
Pattern pattern = Pattern.compile(PHONE_REGEX);
Matcher matcher = pattern.matcher(phone);
return matcher.matches();
// 验证必须带国际区号的手机号格式
Pattern pattern = Pattern.compile(INTERNATIONAL_PHONE_REGEX);
return pattern.matcher(phone).matches();
}
}
@@ -80,6 +80,7 @@ public class ShiroConfig {
filterMap.put("/doc.html", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/user/captcha", "anon");
filterMap.put("/user/smsVerification", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/user/pub-config", "anon");
filterMap.put("/user/register", "anon");
@@ -2,6 +2,7 @@ package xiaozhi.modules.security.controller;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
@@ -24,6 +25,7 @@ import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.security.dto.LoginDTO;
import xiaozhi.modules.security.dto.SmsVerificationDTO;
import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.security.service.SysUserTokenService;
@@ -31,8 +33,10 @@ import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysDictDataService;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.SysDictDataItem;
/**
* 登录控制层
@@ -46,6 +50,7 @@ public class LoginController {
private final SysUserTokenService sysUserTokenService;
private final CaptchaService captchaService;
private final SysParamsService sysParamsService;
private final SysDictDataService sysDictDataService;
@GetMapping("/captcha")
@Operation(summary = "验证码")
@@ -56,22 +61,31 @@ public class LoginController {
captchaService.create(response, uuid);
}
@GetMapping("/smsVerification")
@PostMapping("/smsVerification")
@Operation(summary = "短信验证码")
public void smsVerification(String phone) throws IOException {
// 手机号码不能为空
AssertUtils.isBlank(phone, ErrorCode.PHONE_NOT_NULL);
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
// 验证图形验证码
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误");
}
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SYSTEM_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
}
// 发送短信验证码
captchaService.sendSMSValidateCode(phone);
captchaService.sendSMSValidateCode(dto.getPhone());
return new Result<>();
}
@PostMapping("/login")
@Operation(summary = "登录")
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("验证码错误,请重新获取");
throw new RenException("图形验证码错误,请重新获取");
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
@@ -103,15 +117,18 @@ public class LoginController {
throw new RenException("用户名不是手机号码,请重新输入");
}
// 验证短信验证码是否正常
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getCaptcha());
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
if (!validate) {
throw new RenException("手机验证码错误,请重新获取");
}
} else {
// 验证是否正确输入验证码
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
}
// 判断是否通过验证
if (!validate) {
throw new RenException("验证码错误,请重新获取");
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误,请重新获取");
}
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null) {
@@ -172,8 +189,7 @@ public class LoginController {
throw new RenException("用户名或验证码错误");
}
Long userId = SecurityUser.getUserId();
sysUserService.changePasswordDirectly(userId, dto.getPassword());
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
return new Result<>();
}
@@ -185,6 +201,9 @@ public class LoginController {
.getValueObject(Constant.SysMSMParam.SYSTEM_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class));
config.put("version", Constant.VERSION);
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
List<SysDictDataItem> list = sysDictDataService.getDictDataByType("MOBILE_AREA");
config.put("mobileAreaList", list);
return new Result<Map<String, Object>>().ok(config);
}
}
@@ -25,6 +25,9 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "手机验证码")
private String mobileCaptcha;
@Schema(description = "唯一标识")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;
@@ -0,0 +1,28 @@
package xiaozhi.modules.security.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 短信验证码请求DTO
*/
@Data
@Schema(description = "短信验证码请求")
public class SmsVerificationDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "手机号码")
@NotBlank(message = "{sysuser.username.require}")
private String phone;
@Schema(description = "验证码")
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "唯一标识")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;
}
@@ -18,21 +18,28 @@ public interface CaptchaService {
/**
* 验证码效验
* @param uuid uuid
* @param code 验证码
*
* @param uuid uuid
* @param code 验证码
* @param delete 是否删除验证码
* @return true:成功 false:失败
*/
boolean validate(String uuid, String code);
boolean validate(String uuid, String code, Boolean delete);
/**
* 发送短信验证码
*
* @param phone 手机
*/
void sendSMSValidateCode(String phone);
/**
* 验证短信验证码
* @param phone 手机
*
* @param phone 手机
* @param code 验证码
* @param delete 是否删除验证码
* @return true:成功 false:失败
*/
boolean validateSMSValidateCode(String phone,String code);
boolean validateSMSValidateCode(String phone, String code, Boolean delete);
}
@@ -15,10 +15,13 @@ import com.wf.captcha.base.Captcha;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.sms.service.SmsService;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* 验证码
@@ -29,6 +32,8 @@ public class CaptchaServiceImpl implements CaptchaService {
private RedisUtils redisUtils;
@Resource
private SmsService smsService;
@Resource
private SysParamsService sysParamsService;
@Value("${renren.redis.open}")
private boolean open;
/**
@@ -55,12 +60,12 @@ public class CaptchaServiceImpl implements CaptchaService {
}
@Override
public boolean validate(String uuid, String code) {
public boolean validate(String uuid, String code, Boolean delete) {
if (StringUtils.isBlank(code)) {
return false;
}
// 获取验证码
String captcha = getCache(uuid);
String captcha = getCache(uuid, delete);
// 效验成功
if (code.equalsIgnoreCase(captcha)) {
@@ -72,24 +77,71 @@ public class CaptchaServiceImpl implements CaptchaService {
@Override
public void sendSMSValidateCode(String phone) {
// 检查发送间隔
String lastSendTimeKey = RedisKeys.getSMSLastSendTimeKey(phone);
String lastSendTime = (String) redisUtils.get(lastSendTimeKey);
if (lastSendTime != null) {
long lastSendTimeLong = Long.parseLong(lastSendTime);
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - lastSendTimeLong;
if (timeDiff < 60000) {
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
}
}
// 检查今日发送次数
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
Integer todayCount = (Integer) redisUtils.get(todayCountKey);
if (todayCount == null) {
todayCount = 0;
}
// 获取最大发送次数限制
Integer maxSendCount = sysParamsService.getValueObject(
Constant.SysMSMParam.SYSTEM_SMS_MAX_SEND_COUNT.getValue(),
Integer.class);
if (maxSendCount == null) {
maxSendCount = 5; // 默认值
}
if (todayCount >= maxSendCount) {
throw new RenException("今日发送次数已达上限");
}
String key = RedisKeys.getSMSValidateCodeKey(phone);
String validateCodes = generateValidateCode(6);
setCache(key,validateCodes);
//发送验证码短信
smsService.sendVerificationCodeSms(phone,validateCodes);
// 设置验证码
setCache(key, validateCodes);
// 设置最后发送时间(60秒)
redisUtils.set(lastSendTimeKey, String.valueOf(System.currentTimeMillis()), 60);
// 更新今日发送次数
if (todayCount == 0) {
// 如果是今天第一次发送,设置24小时过期
redisUtils.set(todayCountKey, 1, RedisUtils.DEFAULT_EXPIRE);
} else {
redisUtils.set(todayCountKey, todayCount + 1, RedisUtils.DEFAULT_EXPIRE);
}
// 发送验证码短信
// smsService.sendVerificationCodeSms(phone, validateCodes);
}
@Override
public boolean validateSMSValidateCode(String phone,String code) {
public boolean validateSMSValidateCode(String phone, String code, Boolean delete) {
String key = RedisKeys.getSMSValidateCodeKey(phone);
return validate(key, code);
return validate(key, code, delete);
}
/**
* 生成指定数量的随机数验证码
*
* @param length 数量
* @return 随机码
*/
private String generateValidateCode(Integer length) {
private String generateValidateCode(Integer length) {
String chars = "0123456789"; // 字符范围可以自定义:数字
Random random = new Random();
StringBuilder code = new StringBuilder();
@@ -102,18 +154,19 @@ public class CaptchaServiceImpl implements CaptchaService {
private void setCache(String key, String value) {
if (open) {
key = RedisKeys.getCaptchaKey(key);
// 设置5分钟过期
redisUtils.set(key, value, 300);
} else {
localCache.put(key, value);
}
}
private String getCache(String key) {
private String getCache(String key, Boolean delete) {
if (open) {
key = RedisKeys.getCaptchaKey(key);
String captcha = (String) redisUtils.get(key);
// 删除验证码
if (captcha != null) {
if (captcha != null && delete) {
redisUtils.delete(key);
}
@@ -1,11 +0,0 @@
-- 添加手机短信注册功能的需要的参数
delete from sys_params where id in (601, 602, 610, 611, 612, 613);
INSERT INTO sys_params
(id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date)
VALUES
(601, 'system.enable_mobile_register', 'false', 'boolean', 1, '是否开启手机注册', NULL, NULL, NULL, NULL),
(602, 'system.sms.max_send_count', '5', 'number', 1, '单号码最大短信发送条数', NULL, NULL, NULL, NULL),
(610, 'aliyun.sms.access_key_id', '', 'string', 1, '阿里云平台access_key', NULL, NULL, NULL, NULL),
(611, 'aliyun.sms.access_key_secret', '', 'string', 1, '阿里云平台access_key_secret', NULL, NULL, NULL, NULL),
(612, 'aliyun.sms.sign_name', '', 'string', 1, '阿里云短信签名', NULL, NULL, NULL, NULL),
(613, 'aliyun.sms.sms_code_template_code', '', 'string', 1, '阿里云短信模板', NULL, NULL, NULL, NULL);
@@ -0,0 +1,59 @@
-- 添加手机短信注册功能的需要的参数
delete from sys_params where id in (601, 602, 610, 611, 612, 613);
INSERT INTO sys_params
(id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date)
VALUES
(601, 'system.enable_mobile_register', 'false', 'boolean', 1, '是否开启手机注册', NULL, NULL, NULL, NULL),
(602, 'system.sms.max_send_count', '10', 'number', 1, '单号码单日最大短信发送条数', NULL, NULL, NULL, NULL),
(610, 'aliyun.sms.access_key_id', '', 'string', 1, '阿里云平台access_key', NULL, NULL, NULL, NULL),
(611, 'aliyun.sms.access_key_secret', '', 'string', 1, '阿里云平台access_key_secret', NULL, NULL, NULL, NULL),
(612, 'aliyun.sms.sign_name', '', 'string', 1, '阿里云短信签名', NULL, NULL, NULL, NULL),
(613, 'aliyun.sms.sms_code_template_code', '', 'string', 1, '阿里云短信模板', NULL, NULL, NULL, NULL);
-- 增加手机区域字典
-- 插入固件类型字典类型
delete from `sys_dict_type` where `id` = 102;
INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
(102, 'MOBILE_AREA', '手机区域', '手机区域字典', 0, 1, NOW(), 1, NOW());
-- 插入固件类型字典数据
delete from `sys_dict_data` where `dict_type_id` = 102;
INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
(102001, 102, '中国大陆', '+86', '中国大陆', 1, 1, NOW(), 1, NOW()),
(102002, 102, '中国香港', '+852', '中国香港', 2, 1, NOW(), 1, NOW()),
(102003, 102, '中国澳门', '+853', '中国澳门', 3, 1, NOW(), 1, NOW()),
(102004, 102, '中国台湾', '+886', '中国台湾', 4, 1, NOW(), 1, NOW()),
(102005, 102, '美国/加拿大', '+1', '美国/加拿大', 5, 1, NOW(), 1, NOW()),
(102006, 102, '英国', '+44', '英国', 6, 1, NOW(), 1, NOW()),
(102007, 102, '法国', '+33', '法国', 7, 1, NOW(), 1, NOW()),
(102008, 102, '意大利', '+39', '意大利', 8, 1, NOW(), 1, NOW()),
(102009, 102, '德国', '+49', '德国', 9, 1, NOW(), 1, NOW()),
(102010, 102, '波兰', '+48', '波兰', 10, 1, NOW(), 1, NOW()),
(102011, 102, '瑞士', '+41', '瑞士', 11, 1, NOW(), 1, NOW()),
(102012, 102, '西班牙', '+34', '西班牙', 12, 1, NOW(), 1, NOW()),
(102013, 102, '丹麦', '+45', '丹麦', 13, 1, NOW(), 1, NOW()),
(102014, 102, '马来西亚', '+60', '马来西亚', 14, 1, NOW(), 1, NOW()),
(102015, 102, '澳大利亚', '+61', '澳大利亚', 15, 1, NOW(), 1, NOW()),
(102016, 102, '印度尼西亚', '+62', '印度尼西亚', 16, 1, NOW(), 1, NOW()),
(102017, 102, '菲律宾', '+63', '菲律宾', 17, 1, NOW(), 1, NOW()),
(102018, 102, '新西兰', '+64', '新西兰', 18, 1, NOW(), 1, NOW()),
(102019, 102, '新加坡', '+65', '新加坡', 19, 1, NOW(), 1, NOW()),
(102020, 102, '泰国', '+66', '泰国', 20, 1, NOW(), 1, NOW()),
(102021, 102, '日本', '+81', '日本', 21, 1, NOW(), 1, NOW()),
(102022, 102, '韩国', '+82', '韩国', 22, 1, NOW(), 1, NOW()),
(102023, 102, '越南', '+84', '越南', 23, 1, NOW(), 1, NOW()),
(102024, 102, '印度', '+91', '印度', 24, 1, NOW(), 1, NOW()),
(102025, 102, '巴基斯坦', '+92', '巴基斯坦', 25, 1, NOW(), 1, NOW()),
(102026, 102, '尼日利亚', '+234', '尼日利亚', 26, 1, NOW(), 1, NOW()),
(102027, 102, '孟加拉国', '+880', '孟加拉国', 27, 1, NOW(), 1, NOW()),
(102028, 102, '沙特阿拉伯', '+966', '沙特阿拉伯', 28, 1, NOW(), 1, NOW()),
(102029, 102, '阿联酋', '+971', '阿联酋', 29, 1, NOW(), 1, NOW()),
(102030, 102, '巴西', '+55', '巴西', 30, 1, NOW(), 1, NOW()),
(102031, 102, '墨西哥', '+52', '墨西哥', 31, 1, NOW(), 1, NOW()),
(102032, 102, '智利', '+56', '智利', 32, 1, NOW(), 1, NOW()),
(102033, 102, '阿根廷', '+54', '阿根廷', 33, 1, NOW(), 1, NOW()),
(102034, 102, '埃及', '+20', '埃及', 34, 1, NOW(), 1, NOW()),
(102035, 102, '南非', '+27', '南非', 35, 1, NOW(), 1, NOW()),
(102036, 102, '肯尼亚', '+254', '肯尼亚', 36, 1, NOW(), 1, NOW()),
(102037, 102, '坦桑尼亚', '+255', '坦桑尼亚', 37, 1, NOW(), 1, NOW()),
(102038, 102, '哈萨克斯坦', '+7', '哈萨克斯坦', 38, 1, NOW(), 1, NOW());
@@ -115,9 +115,9 @@ databaseChangeLog:
encoding: utf8
path: classpath:db/changelog/202505091409.sql
- changeSet:
id: 202505141130
id: 202505141132
author: zjy
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505141130.sql
path: classpath:db/changelog/202505141132.sql