java發(fā)送短信系列之限制發(fā)送頻率
本篇是發(fā)送短信的第二部分, 這里我們介紹一下如何限制向同一個用戶(根據(jù)手機號和ip)發(fā)送短信的頻率。
1、使用session
如果是web程序, 那么在session中記錄上次發(fā)送的時間也可以, 但是可以被繞過去. 最簡單的, 直接重啟瀏覽器 或者 清除cache等可以標記session的數(shù)據(jù), 那么就可以繞過session中的記錄. 雖然很多人都不是計算機專業(yè)的, 也沒學過這些. 但是我們需要注意的是, 之所以限制發(fā)送頻率, 是為了防止"短信炸彈", 也就是有人惡意的頻繁的請求向某個手機號碼發(fā)送短信. 所以這個人是有可能懂得這些知識的.
下面我們使用"全局"的數(shù)據(jù)限制向同一個用戶發(fā)送頻率. 我們先做一些"準備"工作
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;
/**
* 判斷短信是否可以發(fā)送.
* @param smsEntity 將要發(fā)送的短信內容
* @return 可以發(fā)送則返回true, 否則返回false
*/
boolean filter(SmsEntity smsEntity);
/**
* 銷毀該過濾器
*/
void destroy();
}
3、主要代碼
限制發(fā)送頻率, 需要記錄某個手機號(IP)及上次發(fā)送短信的時間. 很適合Map去完成, 這里我們先使用ConcurrentMap實現(xiàn):
FrequencyFilter.java
public class FrequencyFilter implements SmsFilter {
/**
* 發(fā)送間隔, 單位: 毫秒
*/
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;
}
/**
* 將發(fā)送時間修改為當前時間.
* 如果距離上次發(fā)送的時間間隔大于{@link #sendInterval}則設置發(fā)送時間為當前時間. 否則不修改任何內容.
*
* @param id 發(fā)送手機號 或 ip
* @return 如果成功將發(fā)送時間修改為當前時間, 則返回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方法中實現(xiàn):
第25-28行: 首先假設用戶是第一次發(fā)送短信, 那么應該把現(xiàn)在的時間放到sendAddressMap中. 如果putIfAbsent返回null, 那么說明用戶確實是第一次發(fā)送短信, 而且現(xiàn)在的時間也已經(jīng)放到了map中, 可以發(fā)送.
第30-33行: 如果用戶不是第一次發(fā)送短信, 那么就需要判斷上次發(fā)送短信的時間和現(xiàn)在的間隔是否小于發(fā)送時間間隔. 如果小于發(fā)送間隔, 那么不能發(fā)送.
第35行: 如果時間間隔足夠大, 那么需要嘗試著將發(fā)送時間設置為當前時間.
- 如果替換成功, 那么可以發(fā)送短信.
- 如果替換失敗, 說明有另外一個線程在本線程執(zhí)行26-35行之間已經(jīng)進行了替換, 也就是說在剛才已經(jīng)發(fā)送了一次短信.
1)、那么可以再重復執(zhí)行25-35行, 確保絕對正確.
2)、也可以直接認為不能發(fā)送, 因為雖然理論上"執(zhí)行26-35行"的時間可能大于"發(fā)送間隔", 但是概率有多大呢? 基本上可以忽略了吧.
這段代碼算是實現(xiàn)了頻率的限制, 但是如果只有"入"而沒有"出"那么sendAddressMap占用的內容會越來越大, 直到產(chǎn)生OutOfMemoryError異常. 下面我們再添加代碼定時清理過期的數(shù)據(jù).
4、清理過期數(shù)據(jù)
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中的所有過期數(shù)據(jù)刪除
*/
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毫秒執(zhí)行一次cleanSendAddressMap方法清理過期數(shù)據(jù).
cleanSendAddressMap方法中首先獲取當前時間, 根據(jù)當前時間獲得一個時間值: 所有在這個時間之后發(fā)送短信的, 現(xiàn)在不可以再次發(fā)送短信. 然后從整個map中刪除所有value小于這個時間值的鍵值對.
當然, 添加上面的代碼后, 最開始的代碼又有bug了: 當最后一行sendAddressMap.replace(id, sendTime, currentTime)執(zhí)行失敗時不一定是其他線程進行了替換, 也有可能是清理線程把數(shù)據(jù)刪了. 所以我們需要修改setSendTime方法最后幾行:
FrequencyFilter.java
private boolean setSendTime(String id) {
// 省略前面的代碼
if(sendAddressMap.replace(id, sendTime, currentTime)) {
return true;
}
return sendAddressMap.putIfAbsent(id, currentTime) == null;
}
這里如果替換成功, 那么直接返回true.
如果替換不成功. 那么可能是其他線程先替換了(第一種情況); 也可能是被清理線程刪除了(第二種情況); 甚至可以能是先被清理線程刪除了, 又有其他線程插入了新的時間值(第三種情況).
- 如果是第一種情況 或者 第三種情況, 那么情況和最開始分析的一樣, 可以直接認為不能發(fā)送.
- 如果是第二種情況, 那么應該是可以發(fā)送的.
- 為了確認是哪種情況, 我們可以執(zhí)行一次putIfAbsent, 如果成功, 說明是第二種情況, 可以發(fā)送; 否則是第一種或者第三種情況, 不能發(fā)送.
至此, 限制發(fā)送時間的代碼就算是完成了. 當然, 這段程序還有一個小bug或者說"特性":
假如, IP為"192.168.0.1"的客戶請求向手機號"12345678900"發(fā)送短信, 然后在sendInterval之內又在IP為"192.168.0.2"的機器上請求向手機號"12345678900"發(fā)送短信. 那么短信將不會發(fā)出去, 而且手機號"12345678900"的上次發(fā)送時間被置為當前時間.
5、使用實例
下面我們提供一個Server層, 展示如何將上一篇以及這一篇中的代碼整合到一起:
SmsService.java
public class SmsService{
private Sms sms;
private List<SmsFilter> filters;
private Properties template;
// 省略了部分代碼
/**
* 發(fā)送驗證碼
*
* @param smsEntity 發(fā)送短信的基本數(shù)據(jù)
* @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;
}
/**
* 發(fā)送注冊驗證碼
*
* @param smsEntity 發(fā)送短信的基本數(shù)據(jù)
*/
private void sendRegisterSms(SmsEntity smsEntity) {
sms.sendMessage(smsEntity.getMobile(),
template.getProperty("register").replace("{captcha}", smsEntity.getCaptcha()));
}
}
之后將FrequencyFilter以及上一篇中的AsyncSmsImpl通過set方法"注入"進去即可。
以上就是本文的全部內容,希望對大家學習java程序設計有所幫助。
相關文章
通過JDBC連接oracle數(shù)據(jù)庫的十大技巧
通過JDBC連接oracle數(shù)據(jù)庫的十大技巧...2006-12-12
深入理解SpringMVC中央調度器DispatcherServlet
這篇文章主要介紹了SpringMVC核心之中央調度器DispatcherServlet的相關知識,包括SpringMVC請求處理過程及SrpingMVC容器和spring?IOC容器關系,需要的朋友可以參考下2022-05-05
Hadoop使用hdfs指令查看hdfs目錄的根目錄顯示被拒的原因及解決方案
這篇文章主要介紹了Hadoop使用hdfs指令查看hdfs目錄的根目錄顯示被拒的原因及解決方案,分布式部署hadoop,服務機只有namenode節(jié)點,主機包含其他所有節(jié)點,本文給大家介紹的非常詳細,需要的朋友可以參考下2023-10-10

