zxdsb666. 发表于 2020-10-29 12:07

一次短信验证码整改实验

# 一次短信验证码整改实验

## 前言:

讨论内容部分为当初的一些短信验证码的需求细节讨论

这个短信验证码在并发量非常大的情况下有可能会失效,后续会进行整改升级,保证线程安全

<!-- more -->

## 需求

短信验证码(要想着怎么把所有的项目都整改起来,不影响原有业务运行) 3天时间,全部替换掉
- 发送短信
- 增加【业务类型】
- 获取短信的时候,增加图片验证码(此处用第三方框架实现)
- 单独增加短信验证码的 ip访问控制 CheckSendSmsIpVisitHelper ,注意别和 CheckIpVisitHelper 冲突
- 校验手机号码长度 11 位
- 60s 根据【手机号码+业务】判断只能发送一次短信,此处将 【手机号码+业务】作为 map 的 key 存储在上下文中
- 可以灵活配置【手机号码+业务】 每天能够获取短信的次数

- 校验短信
- 增加【手机号+短信验证码】的匹配
- 校验成功,清理掉session中存储的信息

# 讨论内容

+验证码就用第三方的。`google kaptcha` 这个可以试试
+这样 jydd apps 都可以用,2-3天可以完全替换掉所有业务的短信。
+页面的修改,直接找建哥提供样式。
+把所有业务都一并改了,省的隔三差五的出报告要整改
+我要一个完全独立的短信验证码模块,虽然不能在按照项目模块来划分,那就完全独立出来包和功能以及依赖
+其实可以直接在apps 的 core里面开发,然后那边直接引用调用?jar包的方式放进去就是的。



## 处理方案:

## 第三方图形验证码参考

(https://www.cnblogs.com/yangzhilong/p/8574685.html)

(https://www.cnblogs.com/FlyHeLanMan/p/6293991.html)

(https://www.ramostear.com/doc/happy-captcha)

### 图形验证码:

1. 调整页面,增加图形验证码的模块(不同模块需要的改动点不一样)

> 也可以先把功能做出来,再让前端根据实际情况去调整样式

1. 尝试增加 happy-captcha 或者 google-captcha (实际查询资料发现没有进行维护)
2. 先不考虑美观问题,以实现功能为主要,后续需要改样式在找前端处理

> 20200903 已实现

### 短信校验

1. 60s 根据【手机号码+业务】判断只能发送一次短信,此处将 【手机号码+业务】作为 map 的 key 存储在上下文中

> 1. 将短信的配置独立到一个单独的xml 文件中,方便 spirngboot 项目以及 spinrgmvc 管理
>2. 业务模块按照模板的格式配置,不跟项目走
> 3. xml 配置读取参考 节假日的xml 配置读取以及作用
> 4. 整个部分可以参考节假日的处理方式,迁移比较方便

使用xml 配置手机+业务模块

2. 校验手机号码长度 11 位

> 1. 写到工具类里面,单独增加一个方法,需要的时候在调用的地方加入(方案一)
> 2. 直接在调用的地方补充添加(方案二)

直接在短信接口加入即可

3. 可以灵活配置【手机号码+业务】 每天能够获取短信的次数

> 1. 其实就是业务模块单个手机号码的限制次数
>
> PS: 目前的攻击手段可以用虚拟手机号码 + 肉鸡服务器 实现,手机号+业务的限制作用个人理解来看作用不明显

4. 大致的工具类设置

+ 初始化读取xml配置
+ 加载一些动态配置到属性里面,包括一些校验次数的限制
+ 单例模式
+ 尽量少的使用三方工具包,尽量使用原生java实现
+ 注意jdk 版本的问题,不以jdk1.8 为基准
+ CheckSendSmsIpVisitHelper 可以是对 CheckIpVisitHelper的扩展
+ (https://www.ramostear.com/doc/happy-captcha) 以此作为参考实现 ,google 的图形化验证码比较老了



## 实现

1. 目前先尝试 使用一下是否可行, 如果可行在进行处理
2. 先不考虑样式问题,先以实现功能保证可用并可以迁移为主
3. 多测试,保证功能稳定,在考虑迁移到apps





### 大致流程

1. 输入手机号码
2. 在点击发送短信按钮之前,弹出输入图形验证码
3. 输入正确的图形验证码,发送短信,图形验证取消,回到输入手机验证码界面
4. 如果输入推行验证码不对,一直重复步骤2

## 问题:

### 1. 绕过图形验证码的接口,直接访问短信接口进行攻击,如何避免?

### 解决方案:

1. 在 【手机号+业务】中增加一个图形验证码的key, 在校验之前,先校验用户当前提交的手机号和图形验证码是否匹配
   1. 匹配:清空用户当前的图形验证码,需要重新请求图形验证码接口才能进行下一次请求
   2. 超时:图形验证码有效时间为60秒,超过60秒需要重新请求图形验证码,重新请求短信接口
   3. 不匹配,返回错误信息
2. 设置校验开关,如果需要在短信接口加入图形验证码的校验,则在发送短信之前,需要当前的图形验证码是否吻合(如果没有图形验证码说明没有经过图形验证这一步骤,直接返回失败)
   1. 图形验证码校验关闭,则不会涉及用户图形验证码和手机号的匹配(考虑临时需要关闭这种校验的情况)
   2. 开启,则会进行上面所说的匹配操作
3. 图形验证码设置为通过校验之后失效,下次请求需要携带新的图形验证码,才能请求通过短信接口





### 2. 增加【手机号-业务】的配置校验

解决方案:

1. 短信模块需要在js请求增加模块参数,如果没有模块参数,视为非法请求
2. 请求带入 手机号-业务-key ,存储当前手机号对应业务的请求次数
3. 增加判断
   1. 如果请求次数在**当天**内超过xml配置次数,将不再允许改手机号对应该接口进行请求,不再发送短信
   2. 但是如果超过了一天之后再次请求,需要将请求次数 **重置为1**,也可以重新发送短信接口请求
4. 在需要的地方调用工具包即可



# 实现过程:

## 成果:

### 控制器调用:

```java
// 开启之后,才做进一步校验
if(PHONE_MODULE_CHECK_ENABLE){
    // 添加 【手机+业务模块】校验 以及 【60秒重复调用校验】
    boolean checkRequest = CheckSendMailHelper.checkContextMap(result, request, phone);
    // 校验不通过的处理办法,可以自定
    if (!checkRequest) {
      return result;
    }
}

//限制用户ip访问短信机获取验证码次数,默认10次
if (IP_CHECK_ENABLE && !CheckIpVisitHelper.check(request)) {
    // 校验不通过的处理办法,可以自定
    result.put("result", SmsRequestStatusEnum.RESULT_STATUS_6.getCode());
    result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_6.getName());
    return result;
}
```

> PHONE_MODULE_CHECK_ENABLE:短信-业务模块校验开关
>
> IP_CHECK_ENABLE:限制短信每天的获取次数,也是手机号+业务模块

### 短信相关的枚举常量:

```java
public enum SmsRequestStatusEnum {

    /**
   * 返回状态码 表示发送正常
   */
    RESULT_STATUS_1(1, "返回状态码 表示发送正常"),
    /**
   * 60s内只能获取不能重复获取验证码
   */
    RESULT_STATUS_2(2, "60s内只能获取不能重复获取验证码"),
    /**
   * 手机号码长度不正确
   */
    RESULT_STATUS_3(3, "手机号码长度不正确"),
//    /**
//   * 用户session已失效
//   */
//    RESULT_STATUS_4(4, "用户session已失效"),
    /**
   * 缺少必要的参数:手机号!
   */
    RESULT_STATUS_4(4, "缺少必要的参数:手机号!"),
    /**
   * 手机号码长度不正确
   */
    RESULT_STATUS_5(5, "手机号码长度不正确"),
    /**
   * 同一个ip请求短信机次数过于频繁
   */
    RESULT_STATUS_6(6, "同一个ip请求短信机次数过于频繁!"),

    /**
   * 60秒内不允许重复请求短信接口
   */
    RESULT_STATUS_7(7, "60秒内不允许重复请求短信接口!"),
    /**
   * 缺少必要的请求参数:短信业务模块名称:phoneModule !
   */
    RESULT_STATUS_9(9, "缺少必要的请求参数:短信业务模块名称:phoneModule !"),
    /**
   * 当前手机号请求超出限制,请等待24小时之后重新请求短信接口
   */
    RESULT_STATUS_10(10, "当前手机号请求次数超出限制,请等待24小时之后重新请求短信接口"),
    /**
   * 图形验证码已失效,请重新请求短信接口!
   */
    RESULT_STATUS_8(8, "图形验证码已失效,请重新请求短信接口!");

    private int code;
    private String name;

    SmsRequestStatusEnum(int code, String name) {
      this.code = code;
      this.name = name;
    }

    public int getCode() {
      return code;
    }

    public String getName() {
      return name;
    }

    public static String getName(int code) {
      for (SmsRequestStatusEnum item : SmsRequestStatusEnum.values()) {
            if (item.getCode() == code) {
                return item.getName();
            }
      }
      return "";
    }
}

```

### IP检测工具类:

```java
/**
* 检测ip访问辅助类,
* 主要处理某个时间段类,
* ip访问次数,以及设置封禁时间、
* 解封等操作,
* 用于防止频繁调用短信机攻击等
* <p>
* <p>
* 重写原理:
* 1. 使用LRUMap key 存储 IP号码,value 存储 访问次数以及时间(使用map)
* 2. 使用servletContext 存储 LRUMap,LRUMap 存储 的 key 为 IP号码-业务模块 VALUE 为 map
* 3. LRUMap 对应的 key IP号码+业务。 value 绑定了访问次数和时间
* 4. 如果没有配置模块,校验将会永久失败,IP的模块和短信的模块使用同一块配置
* 5. ServletContext 生命周期和web的生命周期相同
*
* 2020/09/08 重写工具类,
* 1. 不在暴露 map。
* 2. 使用servletContext 保存 Ip 的 map。Map<String,Object> 形式
* 3. 如果超过IP限制时间,自动进行解锁
*
* @AuThor xd
*/
public class CheckIpVisitHelper {

    /**
   * 日志使用 短信的key
   */
    private static final Logger logger = LoggerFactory.getLogger("phoneCode");

    /**
   * 手机访问限制初始化的值
   */
    private static final int PHONE_REQUEST_INIT_SIZE = 1;

    /**
   * 封禁的时间(单位毫秒)
   */
    private static final int FORBIDEN_TIME = 60 * 1000 * 60;
    /**
   * 超过访问时间重新计时(单位毫秒)
   */
    private static final int MININTEVAL = 60 * 1000 * 60;

    /**
   * LRU Map 初始化大小
   */
    private static final int LRU_MAP_INIT_SIZE = 100000;

    /**
   * IP 在指定时间内的限制次数
   */
    private static final int IP_MAX_VISIST_TIME = Setter.getInt("sms.ip-size");

    /**
   * ip检测使用的 Map key
   */
    private static final String IP_CHECK_MAP = "IP_CHECK_MAP";

    /**
   * 请求次数
   */
    private static final String VISIT_COUNT_KEY = "visit_count";

    /**
   * 最后的请求时间
   */
    private static final String VISIT_TIME_KEY = "visit_time";

    /**
   * IP号码-业务模块名称的格式
   */
    private static final String IP_MOUDULE_FORMAT = "%s-%s";

    /**
   * ip检查工具,将map 放入 ServletContext
   * 1. 检测基于 ServletContext
   * 2. 请附带 phoneModule: 否则校验永远为false
   * map 当中:
   * key: IP号码-业务
   * value:
   * map -> {
   *key: 请求次数:value: int
   *key:请求的时间:value:date
   * }
   *
   * @Param request request请求域
   * @Return 如果校验没有超过限制 返回 true ,否则返回false
   */
    public static boolean check(HttpServletRequest request) {
      String remoteIp = RequestHelper.getRemoteIp(request);
      ServletContext servletContext = request.getServletContext();
      LRUMap attribute = (LRUMap) servletContext.getAttribute(IP_CHECK_MAP);
      if (Objects.isNull(attribute)) {
            attribute = new LRUMap(LRU_MAP_INIT_SIZE);
            servletContext.setAttribute(IP_CHECK_MAP, attribute);
      }
      Date now = new Date();
      // 根据 IP + 业务模块进行绑定
      // 获取请求的模块名称 同时检查是否有配置模块
      String phoneMouduleFlag = CheckSendMailHelper.checkExistsAndGetModule(request);
      if (phoneMouduleFlag == null) {
            return false;
      }
      // IP号码 -业务名称
      String modulePhone = String.format(IP_MOUDULE_FORMAT, remoteIp, phoneMouduleFlag);
      // 获取ip对应的的当前请求次数和请求时间
      Map<String, Object> ipMap = (Map<String, Object>) attribute.get(modulePhone);
      // 如果当前ip没有访问过
      if (MapUtils.isEmpty(ipMap)) {
            ipMap = new HashMap<>();
            ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
            ipMap.put(VISIT_TIME_KEY, now);
            attribute.putIfAbsent(modulePhone, ipMap);
            return true;
      }
      int visitCount = (int) ipMap.get(VISIT_COUNT_KEY);
      Date visitDate = (Date) ipMap.get(VISIT_TIME_KEY);
      // 如果长时间没有访问,重新计算
      if (now.getTime() - visitDate.getTime() > MININTEVAL) {
            ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
            ipMap.put(VISIT_TIME_KEY, now);
            return true;
      }
      // 如果访问的次数超过了限制的次数
      if (visitCount > IP_MAX_VISIST_TIME) {
            // 如果已经到达限制的次数,但是访问时间超过了限制的时间,重新计时,重新计算请求次数
            if (now.getTime() - visitDate.getTime() > FORBIDEN_TIME) {
                ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
                ipMap.put(VISIT_TIME_KEY, now);
                return true;
            }
            logger.info("当前IP: {} 请求次数超过限制", remoteIp);
            return false;
      } else {
            // IP访问次数 + 1
            visitCount++;
            // 更新访问次数
            ipMap.put(VISIT_COUNT_KEY, visitCount);
            // 更新访问时间
            ipMap.put(VISIT_TIME_KEY, now);
      }
      return true;
    }




}
```

> 1. 使用的是servlet-context 全局变量作为存储,依赖web的服务器空间大小,当短信号码过量会造成服务器可访问内存不够,可以考虑用redis 等中间件去存储
> 2. LRUMap:使用最少使用内容作为缓存的设计,存储业务需要判断的手机号等
> 3. 静态方法意味着会出现并发的问题,整个工具类是线程不安全的。

### 短信发送校验工具类:

```java
/**
* 短信发送校验工具类
* map 存储的 key 为手机号码-业务
* value为 发送对象等其他信息
* 包含
* 1. 图形验证码(不开放不做校验)
* 2. 图形验证码有效时间
* 3. 【手机号-业务】 key-name 的配置
* 4. 【手机号-业务-锁定时间】 key-date
*
*
* @Author lazytimes
* @date 2020/09/02 10:21
**/
public class CheckSendMailHelper {

    /**
   * 短信验证码配置
   */
    private static final Logger logger = LoggerFactory.getLogger("phoneCode");

    /**
   * 60 秒内不允许重复请求
   */
    private static final int PHONE_REQUEST_TIME = 60 * 1000;

    /**
   * 60 秒 内 图形验证码有效
   */
    private static final int CAPTCHA_REQUEST_TIME = 60 * 1000;

    /**
   * 用户模块手机号的限制时间 24 小时
   */
    private static final int PHONE_REQUEST_WAIT_TIME = 60 * 1000 * 24 * 60;

    /**
   * 手机访问限制初始化的值
   */
    private static final int PHONE_REQUEST_INIT_SIZE = 1;

    /**
   * 请求上下文的map key
   */
    private static final String CONTEXT_MAP = "CONTEXT_MAP";

    /**
   * 手机号-业务模块名称的格式
   */
    private static final String PHONE_MOUDULE_FORMAT = "%s-%s";


    /**
   * 手机号-业务模块-请求key 的格式标注用户当前模块的请求 定时器
   */
    private static final String PHONE_MOUDULE_TIMER_FORMAT = "%s-%s-timer";

    /**
   * 短信验证码模块的通用格式
   */
    private static final String SMS_MODULE_TEMPLATE = "sms.modules.%s";

    /**
   * 手机号-业务-图形验证码 模块名称的格式
   */
    private static final String CAPTCHA_MOUDULE_FORMAT = "%s-%s-captcha";

    /**
   * 手机号-业务模块-图形验证码-请求key 的格式标注用户当前模块的请求 图形验证码 每个手机号对应业务一份
   */
    private static final String CAPTCHA_MOUDULE_TIMER_FORMAT = "%s-%s-captcha-timer";

    /**
   * 业务模块名称参数Key
   */
    private static final String PHONE_MOUDULE_FLAG = "phoneModule";

    /**
   * 图形验证码key
   */
    private static final String CAPCHACODE = "capchaCode";

//    /**
//   * 最后发送时间key
//   */
//    private static final String LAST_SEND_TIME = "lastSendTime";

    /**
   * 图形验证码开关
   */
    private static final boolean CAPTCHA_ENABLE = Setter.getBoolean("captcha.enable");


    /**
   * 为当前的用户手机号码绑定 图形验证码
   * 图形验证码用于短信接口请求使用,超过一定时间,图形验证码失效
   * 【手机号-业务-图形验证码】:key
   * 【手机号-业务-图形验证码-超时时间】:key
   *
   * @param phoneCode 手机号
   * @param code      图形验证码
   * @param request   请求
   */
    public static void addCapcha(String phoneCode, String code, HttpServletRequest request) {
      if (!CAPTCHA_ENABLE) {
            logger.info("请开启图形验证码校验之后,再配合本工具类方法使用!");
            return;
      }
      ServletContext servletContext = request.getServletContext();
      Map<String, Map<String, Object>> attribute = initServletContextMap(servletContext);
      Date now = new Date();
      // 获取请求的模块名称 同时检查是否有配置模块
      String phoneMouduleFlag = checkExistsAndGetModule(request);
      if (StringUtils.isBlank(phoneMouduleFlag)) {
            return;
      }
      // 手机号 -业务名称
      String modulePhone = String.format(PHONE_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
      // 手机号- 业务名称 - 图形验证码
      String capchaModule = String.format(CAPTCHA_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
      // 手机号 - 业务名称 -图形验证码 - 定时
      String capchaModuleTimer = String.format(CAPTCHA_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
      if (!attribute.containsKey(modulePhone)) {
            HashMap<String, Object> stringObjectHashMap = new HashMap<>();
            stringObjectHashMap.put(capchaModule, code);
            // 图片的有效期
            stringObjectHashMap.put(capchaModuleTimer, now);
            attribute.put(modulePhone, stringObjectHashMap);
      } else {
            Map<String, Object> stringObjectMap = attribute.get(modulePhone);
            // 更新验证码以及有效期
            stringObjectMap.put(capchaModule, code);
            // 图片的有效期
            stringObjectMap.put(capchaModuleTimer, now);
      }
    }

    /**
   * 手机号限制发送处理
   * 1. 增加对于用户请求短信接口的限制,60秒访问一次
   * 2. 增加图形验证码和用户的手机号绑定匹配
   * 1. 图形校验可以灵活开放和关闭
   * 3. 【手机号-业务】的key配置,短信接口当中需要对于用户的请求做限制
   *
   * @param result    封装了返回的状态和信息的 result
   * @param request   请求request
   * @param phoneCode 手机号码
   * @return
   */
    public static boolean checkContextMap(Map<String, Object> result, HttpServletRequest request, String phoneCode) {
      // 获取当前模块配置Map集合
      ServletContext servletContext = request.getServletContext();
      Map<String, Map<String, Object>> attribute = initServletContextMap(servletContext);
      Date now = new Date();
      // 获取请求的模块名称
      String phoneMouduleFlag = checkExistsAndGetModule(request);
      if (phoneMouduleFlag == null) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_9.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_9.getName());
            return false;
      }
      //当前短信业务模块【手机号-业务】
      String modulePhone = String.format(PHONE_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
      // 当前模块【手机号-业务-请求限制时间】
      String modulePhoneTimer = String.format(PHONE_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
      // 当前模块每个用户每天最多请求次数
      int moduleCount = Setter.getInt(String.format(SMS_MODULE_TEMPLATE, phoneMouduleFlag));
      if (!attribute.containsKey(modulePhone)) {
            // 需要自行初始化
            HashMap<String, Object> stringObjectHashMap = new HashMap<>();
            // 初始化短信接口调用次数
            stringObjectHashMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
            // 初始化短信接口调用时间
            stringObjectHashMap.put(modulePhoneTimer, now);
            attribute.put(modulePhone, stringObjectHashMap);
            return true;
      } else {
            Map<String, Object> objectMap = attribute.get(modulePhone);
            // 开启图形验证码校验才做处理
            if (CAPTCHA_ENABLE) {
                if (!checkCatpchaCode(result, request, phoneCode, now, phoneMouduleFlag, objectMap)) {
                  return true;
                }
            }
            // 获取当前【手机号+业务】的对应 访问次数,以及最后的访问时间
            Object count = objectMap.get(modulePhone);
            Object timer = objectMap.get(modulePhoneTimer);
            // 初始化
            if (Objects.isNull(count) || Objects.isNull(timer)) {
                objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
                objectMap.put(modulePhoneTimer, now);
                return true;
            }
            Integer integer = Integer.valueOf(objectMap.get(modulePhone).toString());
            Date time = (Date) timer;
            // 检查当前短信+业务是否在60秒内访问
            if(!checkLastGetTime(result, now, time)){
                return false;
            }
            //如果长时间未访问,重置
            if ((now.getTime() - time.getTime()) > PHONE_REQUEST_WAIT_TIME) {
                //刷新时间
                objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
                objectMap.put(modulePhoneTimer, now);
                return true;
            }
            //   当前模块超过了请求限制
            if (integer > moduleCount) {
                // 超过了请求时间限制,解封
                if (now.getTime() - time.getTime() > PHONE_REQUEST_WAIT_TIME) {
                  //刷新时间
                  objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
                  objectMap.put(modulePhoneTimer, now);
                  return true;
                }
                result.put("result", SmsRequestStatusEnum.RESULT_STATUS_10.getCode());
                result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_10.getName());
                return false;
            }
            // 模块请求次数 + 1
            objectMap.put(modulePhone, integer + PHONE_REQUEST_INIT_SIZE);
            // 刷新时间
            objectMap.put(modulePhoneTimer, now);
      }
      return true;
    }

    /**
   * 校验图形验证码
   *
   * @param result         返回处理结果
   * @param request          请求
   * @param phoneCode      手机号
   * @param now            当前时间
   * @param phoneMouduleFlag 手机号 - 业务模块 标识
   * @param objectMap      servletContext 对象
   * @return
   */
    private static boolean checkCatpchaCode(Map<String, Object> result, HttpServletRequest request, String phoneCode, Date now, String phoneMouduleFlag, Map<String, Object> objectMap) {
      // 手机号- 业务名称 - 图形验证码
      String capchaModule = String.format(CAPTCHA_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
      // 手机号 - 业务名称 -图形验证码 - 定时
      String capchaModuleTimer = String.format(CAPTCHA_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
      // 图形验证码超过60秒失效
      Date captchaCodeValidPeriod = (Date) objectMap.get(capchaModuleTimer);
      // 获取请求参数的验证码
      String requestCaptchaCode = RequestHelper.getString(CAPCHACODE, request);
      // 拿到map中的图形验证码
      Object requestCode = objectMap.get(capchaModule);
      // 是否存在图形验证码的参数,同时比对是否和请求参数一致
      if (StringUtils.isBlank(requestCaptchaCode) || Objects.isNull(requestCode)) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_8.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_8.getName());
            return false;
      }
      // 如果超时或者图形验证码不匹配,需要重新请求图形验证码
      if (!Objects.equals(requestCaptchaCode, requestCode.toString()) || (now.getTime() - captchaCodeValidPeriod.getTime() > (CAPTCHA_REQUEST_TIME))) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_8.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_8.getName());
            return false;
      } else {
            // 清空用户的图形验证码
            objectMap.put(capchaModule, null);
      }
      return true;
    }

    /**
   * 检查最后的访问时间是否在指定时间内容
   *
   * @param result    返回对象结果
   * @param now       当前时间
   * @return
   */
    private static boolean checkLastGetTime(Map<String, Object> result, Date now, Date lastSend) {
      // 60 秒内不允许再次发送
      if ((now.getTime() - lastSend.getTime()) <= PHONE_REQUEST_TIME) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_7.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_7.getName());
            return false;
      }
      return true;
    }

    /**
   * 初始化全局上下文的Map容器
   *
   * @param servletContext 上下文
   * @return 初始化之后的map参数
   */
    private static Map<String, Map<String, Object>> initServletContextMap(ServletContext servletContext) {
      Map<String, Map<String, Object>> attribute = (Map<String, Map<String, Object>>) servletContext.getAttribute(CONTEXT_MAP);
      if (Objects.isNull(attribute)) {
            attribute = new HashMap<>();
            servletContext.setAttribute(CONTEXT_MAP, attribute);
      }
      return attribute;
    }


    /**
   * 检查请求参数中是否存在业务模块配置
   *
   * @param request 请求request
   * @return
   */
    static String checkExistsAndGetModule(HttpServletRequest request) {
      String phoneMouduleFlag = RequestHelper.getString(PHONE_MOUDULE_FLAG, request);
      String moduleNo = Setter.getString(String.format(SMS_MODULE_TEMPLATE, phoneMouduleFlag));
      if (StringUtils.isBlank(moduleNo)) {
            logger.info("未找到对应的短信模块,请在xml配置短信模块名称,并在请求参数中加入 phoneModule: 对应模块名称之后再进行请求");
            return null;
      }
      return phoneMouduleFlag;
    }

}
```

> 1. 使用手机号-业务模块。先获取是否存在对应的模块,然后进行校验
> 2. 图形验证码的方法需要开启推行验证码的情况下,配合使用
> 3. HashMap的线程是不安全的,可以考虑使用ConcurrentHashMap

### 图形验证码的配置:

```xml
<!-- =================================================================== -->
        <!-- 核心:图形验证码的通用配置 -->
        <!-- =================================================================== -->
        <captcha description="图形验证码的通用配置">
                <enable description="是否开放图形验证码" value="false" />
                <length description="设置字符长度" value="5" />
                <!-- 验证码图片的宽度 默认 160 -->
                <width description="设置动画宽度" value="160" />
                <!-- 验证码图片的高度 默认 50 -->
                <height description="设置动画宽度" value="50" />
        </captcha>
```

### 短信验证码的配置:

```xml
<!-- =================================================================== -->
    <!--系统发送短信配置 -->
    <!-- =================================================================== -->
    <sms description="webService短信机服务配置">
      <isopen description="是否开启短信发送" value="true"/>
                <!-- 模块配置: 需要 name 模块名称,用于短信校验 和 value 表示每天最多的请求次数 -->
                <modules>
                        <!-- 注册模块 -->
                        <registered description="注册模块" value="5"/>
                        <!-- 信箱请求短信验证码 -->
                        <mailbox description="信箱模块" value="10"/>
                </modules>
                <ip-size description="ip检测的限制次数" value="10"/>
                <phoneMoudleCheck-enable description="手机号-业务模块校验是否开启" value="true"/>
                <ip-enable description="IP检测开关" value="true"/>
    </sms>
```

# 总结:

1. 工具类基于配置进行开关配置
2. 按照手机号+业务模块,划分同一手机号在不同的业务模块进行校验拦截
3. 图形验证码可以配合短信接口使用,但是目前来看耦合还是有点严重



# 结语:

本人学艺不精,代码写的比较烂,这篇文章算是给自己留坑以后填。

如果看文章费劲头,专门另写一篇说说独立使用。

小小工具类,仅供参考

李里老弟 发表于 2020-10-29 13:13

{:1_893:}支持一下 大牛

额微粒波地 发表于 2020-10-29 13:27

虽然看不到 但是我要支持一下

iamkin 发表于 2020-10-29 15:06

很详细,非常感谢~

李希 发表于 2020-11-28 13:43

厉害,大神

wzh202 发表于 2020-11-30 12:47

留着,收藏了,谢谢!
页: [1]
查看完整版本: 一次短信验证码整改实验