Jelajahi Sumber

1、中康app验证码校验限制
2、中康im错误问题
3、注释掉注册接口

yys 1 Minggu lalu
induk
melakukan
99e1d5fe50

+ 46 - 0
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -8,6 +8,52 @@ import java.util.Base64;
 
 public class PhoneUtil {
 
+    /**
+     * 允许发送短信验证码的大陆正规号段(三大运营商 + 广电,不含虚拟运营商)
+     */
+    private static final String[] SMS_ALLOWED_PREFIXES = {
+            // 中国移动
+            "134", "135", "136", "137", "138", "139",
+            "147", "150", "151", "152", "157", "158", "159",
+            "172", "178", "182", "183", "184", "187", "188",
+            "195", "197", "198",
+            // 中国联通
+            "130", "131", "132", "145", "155", "156", "166",
+            "175", "176", "185", "186", "196",
+            // 中国电信
+            "133", "149", "153", "173", "177", "180", "181", "189",
+            "190", "191", "193", "199",
+            // 中国广电
+            "192"
+    };
+
+    /**
+     * 是否可用于发送短信验证码的大陆手机号
+     */
+    public static boolean isValidSmsPhone(String phone) {
+        if (phone == null || !phone.matches("^1[3-9]\\d{9}$")) {
+            return false;
+        }
+        // 1349 为物联网号段,排除
+        if (phone.startsWith("1349")) {
+            return false;
+        }
+        String prefix = phone.substring(0, 3);
+        for (String allow : SMS_ALLOWED_PREFIXES) {
+            if (allow.equals(prefix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 是否为不可发短信的高风险/非白名单号段(虚拟号、物联网等)
+     */
+    public static boolean isHighRiskSmsPhone(String phone) {
+        return !isValidSmsPhone(phone);
+    }
+
     public static String encryptPhone(String text) {
         String encryptedText=null;
         try {

+ 1 - 1
fs-service/src/main/resources/application-druid-zkzh.yml

@@ -142,7 +142,7 @@ cloud_host:
 openIM:
     secret: openIM123
     userID: imAdmin
-    url: https://webim2.zkhj6.com/api
+    url: https://webim.zkhj6.com/api
 push:
     url: https://fc-mp-413f1468-1770-41a9-8105-23026243f8f1.next.bspapp.com/push1
 #是否使用新im

+ 123 - 38
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -21,6 +21,7 @@ import com.fs.common.service.ISmsService;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.sign.Md5Utils;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.core.config.WxOpenProperties;
 import com.fs.course.domain.LuckyBag;
 import com.fs.course.domain.LuckyBagCollectRecord;
@@ -109,11 +110,20 @@ public class AppLoginController extends AppBaseController{
     @Autowired
     private OpenIMService openIMService;
 
+    /** 同一手机号每天最多发送次数 */
+    private static final int SMS_PHONE_DAILY_LIMIT = 2;
+    /** 同一IP每天最多对多少个不同手机号发注册验证码 */
+    private static final int SMS_IP_DISTINCT_PHONE_LIMIT = 3;
+    /** 注册验证码全站每日总上限(按日常真实注册量收紧,防止换号烧钱) */
+    private static final int SMS_REGISTER_GLOBAL_DAILY_LIMIT = 800;
+    /** 短信验证码有效期(秒),有效期内不允许重发 */
+    private static final int SMS_CODE_EXPIRE_SECONDS = 180;
+
     @ApiOperation("注册app用户")
     @PostMapping("/register")
     @RepeatSubmit
     public R registerDoctor(@Validated @RequestBody FsUserRegisterParam param){
-        FsUser fsUser = findUserByPhone(param.getPhone());
+      /*  FsUser fsUser = findUserByPhone(param.getPhone());
 
 //        if (fsUser == null) {
 //            // 尝试使用加密后的手机号查询
@@ -151,7 +161,7 @@ public class AppLoginController extends AppBaseController{
                 return R.error("注册失败");
             }
         }
-
+*/      return R.ok("当前不允许手机号密码注册!");
     }
 
     @ApiOperation("登录")
@@ -170,9 +180,19 @@ public class AppLoginController extends AppBaseController{
         }
     }
 
+    /**
+     * 注册发送短信验证码(纯后端防轰炸,前端无改动)
+     * <p>
+     * 验证码有效期内不允许重发;输错验证码也不能立刻重发,须等过期后再获取。
+     */
     @PostMapping("/registerSendCode")
     public R registerSendCode(@RequestBody Map<String, String> body){
         String phone = body.get("phone");
+        String phoneCheck = validateSmsPhone(phone);
+        if (phoneCheck != null) {
+            return R.error(phoneCheck);
+        }
+
         String encryptPhone = encryptPhone(phone);
         List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone);
         if(CollectionUtil.isEmpty(user)){
@@ -182,28 +202,85 @@ public class AppLoginController extends AppBaseController{
             return R.error("此电话号码已注册");
         }
 
-        // 验证码 key(存验证码,3分钟有效
-        String smsCodeKey = "sms:code:" + phone;
-        // 发送冷却 key(限制60秒内不能再次发送)
-        String smsCooldownKey = "sms:cooldown:" + phone;
+        // 验证码仍在有效期内:不允许重发(防刷,也避免输错后立刻再发
+        if (StringUtils.isNotEmpty(redisCache.getCacheObject("sms:code:" + phone))) {
+            return R.error("验证码仍在有效期内,请查收短信;如未收到或输入错误,请等待验证码过期后再重新获取");
+        }
 
-        // 判断是否在60秒冷却时间内
-        if (redisCache.getCacheObject(smsCooldownKey) != null) {
-            return R.error("验证码已发送,请稍后再试");
+        String limitMsg = checkRegisterSmsLimit(phone);
+        if (limitMsg != null) {
+            return R.error(limitMsg);
         }
 
-        // 生成新的验证码
         String smsCode = VerifyCodeUtil.generateCode();
-
-        // 发送短信
         smsService.sendCaptcha(phone, smsCode, "验证码");
+        redisCache.setCacheObject("sms:code:" + phone, smsCode, SMS_CODE_EXPIRE_SECONDS, TimeUnit.SECONDS);
+        return R.ok("验证码已发送");
+    }
 
-        // 缓存验证码(3分钟有效)
-        redisCache.setCacheObject(smsCodeKey, smsCode, 180, TimeUnit.SECONDS);
-        // 设置冷却时间(60秒内不能再发)
-        redisCache.setCacheObject(smsCooldownKey, "1", 60, TimeUnit.SECONDS);
+    /**
+     * 校验手机号:格式 + 正规号段白名单(虚拟号/物联网号直接拒绝)
+     */
+    private String validateSmsPhone(String phone) {
+        if (StringUtils.isBlank(phone) || !phone.matches("^1[3-9]\\d{9}$")) {
+            return "手机号格式不正确";
+        }
+        if (!PhoneUtil.isValidSmsPhone(phone)) {
+            logger.warn("拦截非白名单号段短信请求, phone={}, ip={}", phone,
+                    IpUtils.getIpAddr(ServletUtils.getRequest()));
+            return "该手机号暂不支持接收验证码,请更换手机号";
+        }
+        return null;
+    }
 
-        return R.ok("验证码已发送");
+    /**
+     * 注册短信发送限制:冷却 + 单号日限 + IP换号限制 + 全站日预算
+     */
+    private String checkRegisterSmsLimit(String phone) {
+        String cooldownKey = "sms:cooldown:" + phone;
+        if (!redisCache.setIfAbsent(cooldownKey, "1", 60, TimeUnit.SECONDS)) {
+            return "验证码已发送,请稍后再试";
+        }
+
+        String phoneDailyKey = "sms:daily:phone:" + phone;
+        Long phoneDailyCount = redisCache.incr(phoneDailyKey, 1L);
+        if (phoneDailyCount != null && phoneDailyCount == 1L) {
+            redisCache.expire(phoneDailyKey, 1, TimeUnit.DAYS);
+        }
+        if (phoneDailyCount != null && phoneDailyCount > SMS_PHONE_DAILY_LIMIT) {
+            redisCache.deleteObject(cooldownKey);
+            return "今日发送次数已达上限,请明天再试";
+        }
+
+        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        if (StringUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) {
+            String ipPhonesKey = "sms:ip:phones:" + ip;
+            redisCache.redisTemplate.opsForSet().add(ipPhonesKey, phone);
+            Long distinctPhones = redisCache.redisTemplate.opsForSet().size(ipPhonesKey);
+            if (distinctPhones != null && distinctPhones == 1L) {
+                redisCache.expire(ipPhonesKey, 1, TimeUnit.DAYS);
+            }
+            if (distinctPhones != null && distinctPhones > SMS_IP_DISTINCT_PHONE_LIMIT) {
+                redisCache.redisTemplate.opsForSet().remove(ipPhonesKey, phone);
+                redisCache.decr(phoneDailyKey, 1L);
+                redisCache.deleteObject(cooldownKey);
+                logger.warn("拦截同IP换号轰炸, ip={}, phone={}, distinct={}", ip, phone, distinctPhones);
+                return "操作过于频繁,请稍后再试";
+            }
+        }
+
+        String globalKey = "sms:register:global:daily";
+        Long globalCount = redisCache.incr(globalKey, 1L);
+        if (globalCount != null && globalCount == 1L) {
+            redisCache.expire(globalKey, 1, TimeUnit.DAYS);
+        }
+        if (globalCount != null && globalCount > SMS_REGISTER_GLOBAL_DAILY_LIMIT) {
+            redisCache.decr(phoneDailyKey, 1L);
+            redisCache.deleteObject(cooldownKey);
+            logger.error("注册短信全站日限额已用尽, count={}", globalCount);
+            return "系统繁忙,请稍后再试";
+        }
+        return null;
     }
 
 
@@ -223,10 +300,10 @@ public class AppLoginController extends AppBaseController{
         }
         String redisCode = redisCache.getCacheObject("sms:code:" + phone);
         if (StringUtils.isEmpty(redisCode)){
-            return R.error("验证码已过期,请重新发送");
+            return R.error("验证码已过期,请重新获取");
         }
         if (!redisCode.equals(code)) {
-            return R.error("验证码错误");
+            return R.error("验证码错误,请输入正确验证码;如需重新获取,请等待当前验证码过期后再试");
         }
         FsUser user = new FsUser();
         // 创建新用户
@@ -240,6 +317,7 @@ public class AppLoginController extends AppBaseController{
         user.setCreateTime(new Date());
         user.setAppCreateTime(new Date());
         if (userService.insertFsUser(user) > 0) {
+            redisCache.deleteObject("sms:code:" + phone);
             return R.ok("注册成功");
         } else {
             return R.error("注册失败");
@@ -578,10 +656,10 @@ public class AppLoginController extends AppBaseController{
         }
         String redisCode = redisCache.getCacheObject("sms:code:" + phone);
         if (StringUtils.isEmpty(redisCode)){
-            return R.error("验证码已过期,请重新发送");
+            return R.error("验证码已过期,请重新获取");
         }
         if (!redisCode.equals(code)) {
-            return R.error("验证码错误");
+            return R.error("验证码错误,请输入正确验证码;如需重新获取,请等待当前验证码过期后再试");
         }
 
         FsUser info=user.get(0);
@@ -645,10 +723,10 @@ public class AppLoginController extends AppBaseController{
         }
         String redisCode = redisCache.getCacheObject("sms:code:" + phone);
         if (StringUtils.isEmpty(redisCode)){
-            return R.error("验证码已过期,请重新发送");
+            return R.error("验证码已过期,请重新获取");
         }
         if (!redisCode.equals(code)) {
-            return R.error("验证码错误");
+            return R.error("验证码错误,请输入正确验证码;如需重新获取,请等待当前验证码过期后再试");
         }
         String password = Md5Utils.hash(newPassword);
         return userService.updatePasswordByPhone(password,encryptPhone);
@@ -1042,6 +1120,11 @@ public class AppLoginController extends AppBaseController{
     @PostMapping("/sendCode")
     public R sendCode(@RequestBody Map<String, String> body){
         String phone = body.get("phone");
+        String phoneCheck = validateSmsPhone(phone);
+        if (phoneCheck != null) {
+            return R.error(phoneCheck);
+        }
+
         String encryptPhone = encryptPhone(phone);
         List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone);
         if(CollectionUtil.isEmpty(user)){
@@ -1051,27 +1134,29 @@ public class AppLoginController extends AppBaseController{
             return R.error("此电话号码未绑定用户");
         }
 
-        // 验证码 key(3分钟有效)
-        String smsCodeKey = "sms:code:" + phone;
-        // 冷却 key(60秒内不能重复发送)
-        String smsCooldownKey = "sms:cooldown:" + phone;
+        // 验证码仍在有效期内:不允许重发
+        if (StringUtils.isNotEmpty(redisCache.getCacheObject("sms:code:" + phone))) {
+            return R.error("验证码仍在有效期内,请查收短信;如未收到或输入错误,请等待验证码过期后再重新获取");
+        }
 
-        // 判断是否在 60 秒冷却期
-        if (redisCache.getCacheObject(smsCooldownKey) != null) {
+        String smsCooldownKey = "sms:cooldown:" + phone;
+        if (!redisCache.setIfAbsent(smsCooldownKey, "1", 60, TimeUnit.SECONDS)) {
             return R.error("验证码已发送,请稍后再试");
         }
 
-        // 生成新验证码
-        String smsCode = VerifyCodeUtil.generateCode();
+        String phoneDailyKey = "sms:daily:phone:" + phone;
+        Long phoneDailyCount = redisCache.incr(phoneDailyKey, 1L);
+        if (phoneDailyCount != null && phoneDailyCount == 1L) {
+            redisCache.expire(phoneDailyKey, 1, TimeUnit.DAYS);
+        }
+        if (phoneDailyCount != null && phoneDailyCount > SMS_PHONE_DAILY_LIMIT) {
+            redisCache.deleteObject(smsCooldownKey);
+            return R.error("今日发送次数已达上限,请明天再试");
+        }
 
-        // 发送短信
+        String smsCode = VerifyCodeUtil.generateCode();
         smsService.sendCaptcha(phone, smsCode, "验证码");
-
-        // 缓存验证码(3分钟有效)
-        redisCache.setCacheObject(smsCodeKey, smsCode, 180, TimeUnit.SECONDS);
-        // 设置冷却时间(60秒内不能再发)
-        redisCache.setCacheObject(smsCooldownKey, "1", 60, TimeUnit.SECONDS);
-
+        redisCache.setCacheObject("sms:code:" + phone, smsCode, SMS_CODE_EXPIRE_SECONDS, TimeUnit.SECONDS);
         return R.ok("验证码已发送");
     }