java发送短信系列之限制发送频率

本篇是发送短信的第二部分, 这里我们介绍一下如何限制向同一个用户(根据手机号和ip)发送短信的频率。

1、使用session

如果是web程序, 那么在session中记录上次发送的时间也可以, 但是可以被绕过去. 最简单的, 直接重启浏览器 或者 清除cache等可以标记session的数据, 那么就可以绕过session中的记录. 虽然很多人都不是计算机专业的, 也没学过这些. 但是我们需要注意的是, 之所以限制发送频率, 是为了防止"短信炸弹", 也就是有人恶意的频繁的请求向某个手机号码发送短信. 所以这个人是有可能懂得这些知识的.

下面我们使用"全局"的数据限制向同一个用户发送频率. 我们先做一些"准备"工作

2、定义接口、实体类

我们需要的实体类如下:

SmsEntity.java

public class SmsEntity{
  private Integer id;
  private String mobile;
  private String ip;
  private Integer type;
  private Date time;
  private String captcha;

  // 省略构造方法和getter、setter方法
}

过滤接口如下:

SmsFilter.java

public interface SmsFilter {

  /**
   * 初始化该过滤器
   */
  void init() throws Exception;

  /**
   * 判断短信是否可以发送.
   * @param smsEntity 将要发送的短信内容
   * @return 可以发送则返回true, 否则返回false
   */
  boolean filter(SmsEntity smsEntity);

  /**
   * 销毁该过滤器
   */
  void destroy();

}

3、主要代码

限制发送频率, 需要记录某个手机号(IP)及上次发送短信的时间. 很适合Map去完成, 这里我们先使用ConcurrentMap实现:

FrequencyFilter.java

public class FrequencyFilter implements SmsFilter {
  /**
   * 发送间隔, 单位: 毫秒
   */
  private long sendInterval;
  private ConcurrentMap<String, Long> sendAddressMap = new ConcurrentHashMap<>();

  // 省略了部分无用代码

  @Override
  public boolean filter(SmsEntity smsEntity) {
    if(setSendTime(smsEntity.getMobile()) && setSendTime(smsEntity.getIp())){
      return true;
    }
    return false;
  }

  /**
   * 将发送时间修改为当前时间.
   * 如果距离上次发送的时间间隔大于{@link #sendInterval}则设置发送时间为当前时间. 否则不修改任何内容.
   *
   * @param id 发送手机号 或 ip
   * @return 如果成功将发送时间修改为当前时间, 则返回true. 否则返回false
   */
  private boolean setSendTime(String id) {
    long currentTime = System.currentTimeMillis();

    Long sendTime = sendAddressMap.putIfAbsent(id, currentTime);
    if(sendTime == null) {
      return true;
    }

    long nextCanSendTime = sendTime + sendInterval;
    if(currentTime < nextCanSendTime) {
      return false;
    }

    return sendAddressMap.replace(id, sendTime, currentTime);
  }
}

这里, 主要的逻辑在setSendTime方法中实现:

第25-28行: 首先假设用户是第一次发送短信, 那么应该把现在的时间放到sendAddressMap中. 如果putIfAbsent返回null, 那么说明用户确实是第一次发送短信, 而且现在的时间也已经放到了map中, 可以发送.

第30-33行: 如果用户不是第一次发送短信, 那么就需要判断上次发送短信的时间和现在的间隔是否小于发送时间间隔. 如果小于发送间隔, 那么不能发送.

第35行: 如果时间间隔足够大, 那么需要尝试着将发送时间设置为当前时间.

  • 如果替换成功, 那么可以发送短信.
  • 如果替换失败, 说明有另外一个线程在本线程执行26-35行之间已经进行了替换, 也就是说在刚才已经发送了一次短信.

1)、那么可以再重复执行25-35行, 确保绝对正确.
2)、也可以直接认为不能发送, 因为虽然理论上"执行26-35行"的时间可能大于"发送间隔", 但是概率有多大呢? 基本上可以忽略了吧.
这段代码算是实现了频率的限制, 但是如果只有"入"而没有"出"那么sendAddressMap占用的内容会越来越大, 直到产生OutOfMemoryError异常. 下面我们再添加代码定时清理过期的数据.

4、清理过期数据

FrequencyFilter.java

/**
 * 在上面代码的基础上, 再添加如下代码
 */
public class FrequencyFilter implements SmsFilter {
  private long cleanMapInterval;
  private Timer timer = new Timer("sms_frequency_filter_clear_data_thread");

  @Override
  public void init() {
    timer.schedule(new TimerTask() {
      @Override
      public void run() {
        cleanSendAddressMap();
      }
    }, cleanMapInterval, cleanMapInterval);
  }

  /**
   * 将sendAddressMap中的所有过期数据删除
   */
  private void cleanSendAddressMap() {
    long currentTime = System.currentTimeMillis();
    long expireSendTime = currentTime - sendInterval;

    for(String key : sendAddressMap.keySet()) {
      Long sendTime = sendAddressMap.get(key);
      if(sendTime < expireSendTime) {
        sendAddressMap.remove(key, sendTime);
      }
    }
  }

  @Override
  public void destroy() {
    timer.cancel();
  }
}

这段程序不算复杂, 启动一个定时器, 每隔cleanMapInterval毫秒执行一次cleanSendAddressMap方法清理过期数据.

cleanSendAddressMap方法中首先获取当前时间, 根据当前时间获得一个时间值: 所有在这个时间之后发送短信的, 现在不可以再次发送短信. 然后从整个map中删除所有value小于这个时间值的键值对.

当然, 添加上面的代码后, 最开始的代码又有bug了: 当最后一行sendAddressMap.replace(id, sendTime, currentTime)执行失败时不一定是其他线程进行了替换, 也有可能是清理线程把数据删了. 所以我们需要修改setSendTime方法最后几行:

FrequencyFilter.java

private boolean setSendTime(String id) {
  // 省略前面的代码
  if(sendAddressMap.replace(id, sendTime, currentTime)) {
    return true;
  }
  return sendAddressMap.putIfAbsent(id, currentTime) == null;
}

这里如果替换成功, 那么直接返回true.

如果替换不成功. 那么可能是其他线程先替换了(第一种情况); 也可能是被清理线程删除了(第二种情况); 甚至可以能是先被清理线程删除了, 又有其他线程插入了新的时间值(第三种情况).

  • 如果是第一种情况 或者 第三种情况, 那么情况和最开始分析的一样, 可以直接认为不能发送.
  • 如果是第二种情况, 那么应该是可以发送的.
  • 为了确认是哪种情况, 我们可以执行一次putIfAbsent, 如果成功, 说明是第二种情况, 可以发送; 否则是第一种或者第三种情况, 不能发送.

至此, 限制发送时间的代码就算是完成了. 当然, 这段程序还有一个小bug或者说"特性":

假如, IP为"192.168.0.1"的客户请求向手机号"12345678900"发送短信, 然后在sendInterval之内又在IP为"192.168.0.2"的机器上请求向手机号"12345678900"发送短信. 那么短信将不会发出去, 而且手机号"12345678900"的上次发送时间被置为当前时间.

5、使用实例

下面我们提供一个Server层, 展示如何将上一篇以及这一篇中的代码整合到一起:

SmsService.java

public class SmsService{
  private Sms sms;
  private List<SmsFilter> filters;
  private Properties template;

  // 省略了部分代码

  /**
   * 发送验证码
   *
   * @param smsEntity 发送短信的基本数据
   * @return 如果提交成功, 返回0. 否则返回其他值.
   */
  public int sendCaptcha(SmsEntity smsEntity){
    for(SmsFilter filter : filters) {
      if(!filter.filter(smsEntity)){
        return 1;
      }
    }
    if(SmsEntity.REGISTER_TYPE.equals(smsEntity.getType())) {
      sendRegisterSms(smsEntity);
    }
    else{
      return 2;
    }
    return 0;
  }

  /**
   * 发送注册验证码
   *
   * @param smsEntity 发送短信的基本数据
   */
  private void sendRegisterSms(SmsEntity smsEntity) {
    sms.sendMessage(smsEntity.getMobile(),
        template.getProperty("register").replace("{captcha}", smsEntity.getCaptcha()));
  }

}

之后将FrequencyFilter以及上一篇中的AsyncSmsImpl通过set方法"注入"进去即可。

以上就是本文的全部内容,希望对大家学习java程序设计有所帮助。

(0)

相关推荐

  • Java使用云片API发送短信验证码

    下面开始介绍的是如何利用机器完成批量操作,将短信业务自动化. 获取APIKEY 云片网提供了完整的SDK和API,可以帮助开发者快速完成业务开发. 在开始Coding前,需要先获取APIKEY,如下所示. 获取APIKEY 点击眼睛按钮后,输入验证码,就可以查看APIKEY了. 这里需要说明的是,APIKEY特别重要,一定要保护好它,避免泄露.云片这边提供了几重保护机制,例如验证.敏感处理.子账号独立APIKEY等,看得出来他们的安全意识还是挺不错的. 开始Coding 有了APIKEY,就可以

  • java发送短信的实现步骤

    一.在中国网建中注册用户:本程序是通过中国网建提供的SMS短信平台实现的,该平台新用户注册可以拥有免费5条普通短信和3条彩信,足够进行尝试和体验了.中国网建注册地址:http://sms.webchinese.cn/reg.shtml: 二.修改短信签名:注册成功后登陆,用户登陆有首先要修改短信签名,因为中国网建中规定了,发送的短信如果没有正规的签名是不能成功发送的,提示性信息见下图: 修改短信签名的步骤:用户信息修改--->修改用户信息--->保存信息,如下图: 三.修改验证码网关和绑定短信

  • java使用smslib连接短信猫发送短信代码分享

    复制代码 代码如下: import java.util.ArrayList;import java.util.List; import org.apache.log4j.Logger;import org.smslib.ICallNotification;import org.smslib.IInboundMessageNotification;import org.smslib.IOutboundMessageNotification;import org.smslib.InboundMess

  • java发送短信系列之限制日发送次数

    在前两篇文章中, 我们实现了同步/异步发送短信以及限制发送短信频率.这一篇, 我们介绍一下限制每日向同一个用户(根据手机号和ip判断)发送短信的次数 1.数据表结构 由于需要记录整天的发送记录, 因此这里我们将数据保存到数据库中. 数据表结构如下: type为验证码的类型, 比如注册, 重置密码等. sendTime的默认值为当前时间. 2.限制日发送次数 我们这里需要用到上一篇中提到的接口和实体类. DailyCountFilter.java public class DailyCountFi

  • java使用短信设备发送sms短信的示例(java发送短信)

    复制代码 代码如下: import gnu.io.*;import java.util.*;import java.io.*; public class CommTest{    static CommPortIdentifier portId;    static Enumeration portList;    static int bauds[] = { 9600, 19200, 57600, 115200 };    //检测端口所支持的波特率 public static void ma

  • java发送短信系列之同步、异步发送短信

    本篇本章是发送短信的第一部分, 说一下同步/异步发送短信的代码, 以后几篇我们稍微完善一下功能, 添加发送频率的限制和日发送次数的限制. 发送短信的方法可能不少, 我们的方法是使用服务商提供的服务. 一般来说, 这些服务都是和语言无关的, 这里我们使用java写示例程序. 1.发送短信的接口 根据自己的情况选择服务商. 2.开发文档 从开发文档中我们可以看到. 可以直接使用http请求也可以使用WebService请求发送短信. 由于DEMO文件夹下的java和jsp文件夹中的代码都是使用htt

  • JAVA实现利用第三方平台发送短信验证码

    前段时间自己做的一个小项目中,涉及到用短信验证码登录.注册的问题,之前没涉及过这一块,看了别人的博客其实也是似懂非懂的,现在就将自己做的利用第三方短信平台来发送验证码这个功能记下来. 本文以注册为例,在SpringMVC+Spring+Mybatis框架的基础上完成该短信验证码功能. 发送短信验证码的原理是:随机生成一个6位数字,将该6位数字保存到session当中,客户端通过sessionid判断对应的session,用户输入的验证码再与session记录的验证码进行比较. 为了防止有广告嫌疑

  • java发送短信系列之限制发送频率

    本篇是发送短信的第二部分, 这里我们介绍一下如何限制向同一个用户(根据手机号和ip)发送短信的频率. 1.使用session 如果是web程序, 那么在session中记录上次发送的时间也可以, 但是可以被绕过去. 最简单的, 直接重启浏览器 或者 清除cache等可以标记session的数据, 那么就可以绕过session中的记录. 虽然很多人都不是计算机专业的, 也没学过这些. 但是我们需要注意的是, 之所以限制发送频率, 是为了防止"短信炸弹", 也就是有人恶意的频繁的请求向某个

  • Java实现发送短信验证码+redis限制发送的次数功能

    java实现短信验证码发送,由于我们使用第三方平台进行验证码的发送,所以首先,我们要在一个平台进行注册.这样的平台有很多,有的平台在新建账号的时候会附带赠几条免费短信.这里我仅做测试使用(具体哪个平台见参考三,很简单,注册账号就行,记得添加短信签名). 另外,在实际项目中,如果有人恶意攻击,不停的发送短信验证码,就会造成很大的损失.故对发送次数做一定的限制就非常必要,这里我们限制一个手机号一天可以发多少短信和短信平台无关. 这里采用的是存redis来实现这一个功能.就是每次调用发送验证码这个接口

  • Java实现发送短信验证码功能

    一个发送短信验证码的功能,使用的是信易通的短信平台接口,然后在Java中使用HttpClient模拟POST请求或者GET请求(看短信平台要求,一般的情况下都是POST请求),调用短信平台提供的接口(遵循短信平台的接口规范即可).具体看代码: 使用HttpClient的时候需要在项目中引入: commons-httpclient-3.1.jar 这个jar包, 项目结构: 1.创建一个Http的模拟请求工具类,然后写一个POST方法或者GET方法 /** * 文件说明 * @Descriptio

  • java实现发送短信验证码

    最近用学习了一下调用第三方接口发送短信验证码的程序,希望能够帮助到大家. 1.首先下图为项目的目录结构,需要带入三个包: commons-httpclient-3.1.jar commons-logging-1.0.4.jar codec-1.3.jar 2.其次要创建模拟POST.GET请求的工具类: package com.demo.util; import java.io.IOException; import java.util.Map; import org.apache.common

随机推荐