feat(upms): 增强短信验证码注册功能和安全机制

- 在RegisterUserDTO中新增短信验证码字段
- 用户注册时增加短信验证码校验逻辑
- 合并短信验证码发送接口为单一API
- 增加IP频率限制防止短信轰炸攻击
- 优化验证码校验失败的错误返回类型
This commit is contained in:
PIGCLOUD committed 2025-12-30 10:46:02 +08:00
1 parent e959811fa2
commit 13d17923e4
4 files changed
+46 -24

No files matched your search

@@ -36,4 +36,9 @@ public class RegisterUserDTO {
*/
private Long tenantId;
/**
* 短信验证码
*/
private String code;
}
@@ -182,26 +182,16 @@ public class SysMessageController {
}
/**
* 发送短信(验证码)
* 发送短信验证码
*
* @param mobile 手机号 (需要注册,对外提供,安全性第一)
* @return {@link R }
* @param mobile 手机号
* @param registered 是否已注册,默认true
* @return 发送结果
*/
@Inner(value = false)
@GetMapping("/send/smsCode")
public R sendSmsCode(@RequestParam String mobile) {
return sysMessageService.sendSmsCode(mobile, true);
}
/**
* 发送内部短信验证码
*
* @param mobile 手机号码 (系统内不一定存在)
* @return 发送结果
*/
@GetMapping("/send/smsInnerCode")
public R sendSmsInnerCode(@RequestParam String mobile) {
return sysMessageService.sendSmsCode(mobile, false);
public R sendSmsCode(@RequestParam String mobile, @RequestParam(required = false, defaultValue = "true") boolean registered) {
return sysMessageService.sendSmsCode(mobile, registered);
}
/**
@@ -234,12 +224,12 @@ public class SysMessageController {
*/
@PostMapping("/send/hook")
public R sendWebhook(@Valid @RequestBody MessageHookDTO messageHookDTO) {
return sysMessageService.sendHook(messageHookDTO);
}
return sysMessageService.sendHook(messageHookDTO);
}
@GetMapping("/list/hook")
public R listHookBizCode(MessageHookDTO messageHookDTO) {
return sysMessageService.listHookBizCode(messageHookDTO);
}
@GetMapping("/list/hook")
public R listHookBizCode(MessageHookDTO messageHookDTO) {
return sysMessageService.listHookBizCode(messageHookDTO);
}
}
@@ -42,7 +42,9 @@ import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import com.pig4cloud.pigx.common.core.exception.CheckedException;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.core.util.WebUtils;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import com.pig4cloud.pigx.common.file.core.FileTemplate;
@@ -279,12 +281,27 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
*/
@Override
public R<Boolean> sendSmsCode(String mobile, boolean registered) {
// IP 限制校验,单位时间内同一IP发送次数限制
String clientIp = WebUtils.getIP();
String ipLimitKey = CacheConstants.DEFAULT_CODE_KEY + "IP_LIMIT:" + clientIp;
if (StrUtil.isNotBlank(clientIp)) {
Long maxTimes = ParamResolver.getLong("SMS_IP_LIMIT_TIMES", 5L);
Long ipSendTimes = RedisUtils.increment(ipLimitKey, 1L);
// 设置过期时间
RedisUtils.expire(ipLimitKey, SecurityConstants.CODE_TIME);
if (ipSendTimes > maxTimes) {
log.info("IP发送验证码过于频繁:{}, 已发送{}次", clientIp, ipSendTimes);
return R.failed(Boolean.FALSE, MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_OFTEN));
}
}
List<SysUser> userList = TenantBroker
.noneAs(() -> userMapper.selectList(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, mobile)));
if (registered && CollUtil.isEmpty(userList)) {
log.info("手机号未注册:{}", mobile);
return R.ok(Boolean.FALSE, MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_PHONE_UNREGISTERED, mobile));
return R.failed(Boolean.FALSE, MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_PHONE_UNREGISTERED, mobile));
}
String codeObj = RedisUtils
@@ -292,7 +309,7 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
if (StrUtil.isNotBlank(codeObj)) {
log.info("手机号验证码未过期:{},{}", mobile, codeObj);
return R.ok(Boolean.FALSE, MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_OFTEN));
return R.failed(Boolean.FALSE, MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_OFTEN));
}
String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
@@ -533,6 +533,16 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> registerUser(RegisterUserDTO userDto) {
// 校验短信验证码
if (StrUtil.isBlank(userDto.getCode())) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_PARAM_ILLEGAL));
}
String codeObj = RedisUtils
.get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone());
if (!StrUtil.equals(codeObj, userDto.getCode())) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR));
}
// 判断用户名是否存在
boolean usernameExists = TenantBroker
.noneAs(() -> this.exists(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, userDto.getUsername())));