本篇是發送短信的第二部分, 這裡我們介紹一下如何限制向同一個用戶(根據手機號和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行: 如果時間間隔足夠大, 那麼需要嘗試著將發送時間設置為當前時間.
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.
如果替換不成功. 那麼可能是其他線程先替換了(第一種情況); 也可能是被清理線程刪除了(第二種情況); 甚至可以能是先被清理線程刪除了, 又有其他線程插入了新的時間值(第三種情況).
至此, 限制發送時間的代碼就算是完成了. 當然, 這段程序還有一個小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程序設計有所幫助。